fetch(modify):修改原来的bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m53s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m53s
This commit is contained in:
@@ -84,9 +84,9 @@
|
||||
|
||||
<!-- 分配角色对话框 -->
|
||||
<ElDialog v-model="roleDialogVisible" title="分配角色" width="500px">
|
||||
<ElRadioGroup v-model="selectedRole" class="role-radio-group">
|
||||
<div v-for="role in allRoles" :key="role.ID" class="role-radio-item">
|
||||
<ElRadio :label="role.ID">
|
||||
<ElCheckboxGroup v-model="selectedRoles">
|
||||
<div v-for="role in allRoles" :key="role.ID" style="margin-bottom: 12px">
|
||||
<ElCheckbox :label="role.ID">
|
||||
{{ role.role_name }}
|
||||
<ElTag
|
||||
:type="role.role_type === 1 ? 'primary' : 'success'"
|
||||
@@ -95,9 +95,9 @@
|
||||
>
|
||||
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
||||
</ElTag>
|
||||
</ElRadio>
|
||||
</ElCheckbox>
|
||||
</div>
|
||||
</ElRadioGroup>
|
||||
</ElCheckboxGroup>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="roleDialogVisible = false">取消</ElButton>
|
||||
@@ -114,7 +114,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { FormInstance, ElSwitch } from 'element-plus'
|
||||
import { FormInstance, ElSwitch, ElCheckbox, ElCheckboxGroup, ElTag } from 'element-plus'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
@@ -134,7 +134,7 @@
|
||||
const loading = ref(false)
|
||||
const roleSubmitLoading = ref(false)
|
||||
const currentAccountId = ref<number>(0)
|
||||
const selectedRole = ref<number | undefined>(undefined)
|
||||
const selectedRoles = ref<number[]>([])
|
||||
const allRoles = ref<PlatformRole[]>([])
|
||||
|
||||
// 定义表单搜索初始值
|
||||
@@ -292,7 +292,8 @@
|
||||
activeText: getStatusText(CommonStatus.ENABLED),
|
||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||
inlinePrompt: true,
|
||||
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
|
||||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||
handleStatusChange(row, val as number)
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -356,15 +357,18 @@
|
||||
// 显示分配角色对话框
|
||||
const showRoleDialog = async (row: any) => {
|
||||
currentAccountId.value = row.ID
|
||||
selectedRole.value = undefined
|
||||
selectedRoles.value = []
|
||||
|
||||
// 先加载当前账号的角色,再打开对话框
|
||||
try {
|
||||
// 每次打开对话框时重新加载最新的角色列表
|
||||
await loadAllRoles()
|
||||
|
||||
// 先加载当前账号的角色,再打开对话框
|
||||
const res = await AccountService.getAccountRoles(row.ID)
|
||||
if (res.code === 0) {
|
||||
// 提取角色ID(只取第一个角色)
|
||||
// 提取角色ID数组
|
||||
const roles = res.data || []
|
||||
selectedRole.value = roles.length > 0 ? roles[0].ID : undefined
|
||||
selectedRoles.value = roles.map((role: any) => role.ID)
|
||||
// 数据加载完成后再打开对话框
|
||||
roleDialogVisible.value = true
|
||||
}
|
||||
@@ -375,17 +379,13 @@
|
||||
|
||||
// 提交分配角色
|
||||
const handleAssignRoles = async () => {
|
||||
if (selectedRole.value === undefined) {
|
||||
ElMessage.warning('请选择一个角色')
|
||||
return
|
||||
}
|
||||
|
||||
roleSubmitLoading.value = true
|
||||
try {
|
||||
// 将单个角色ID包装成数组传给后端
|
||||
await AccountService.assignRolesToAccount(currentAccountId.value, [selectedRole.value])
|
||||
await AccountService.assignRolesToAccount(currentAccountId.value, selectedRoles.value)
|
||||
ElMessage.success('分配角色成功')
|
||||
roleDialogVisible.value = false
|
||||
// 刷新列表以更新角色显示
|
||||
await getAccountList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
@@ -503,13 +503,4 @@
|
||||
.account-page {
|
||||
// 账号管理页面样式
|
||||
}
|
||||
|
||||
.role-radio-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.role-radio-item {
|
||||
padding: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -328,7 +328,8 @@
|
||||
activeText: '启用',
|
||||
inactiveText: '禁用',
|
||||
inlinePrompt: true,
|
||||
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
|
||||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||
handleStatusChange(row, val as number)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<ArtTableFullScreen>
|
||||
<div class="enterprise-cards-page" id="table-full-screen">
|
||||
<!-- 企业信息卡片 -->
|
||||
<ElCard shadow="never" style="margin-bottom: 16px">
|
||||
<ElCard shadow="never" class="enterprise-info-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>企业信息</span>
|
||||
@@ -10,8 +10,12 @@
|
||||
</div>
|
||||
</template>
|
||||
<ElDescriptions :column="3" border v-if="enterpriseInfo">
|
||||
<ElDescriptionsItem label="企业名称">{{ enterpriseInfo.enterprise_name }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="企业编号">{{ enterpriseInfo.enterprise_code }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="企业名称">{{
|
||||
enterpriseInfo.enterprise_name
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="企业编号">{{
|
||||
enterpriseInfo.enterprise_code
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="联系人">{{ enterpriseInfo.contact_name }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCard>
|
||||
@@ -67,92 +71,51 @@
|
||||
<ElDialog
|
||||
v-model="allocateDialogVisible"
|
||||
title="授权卡给企业"
|
||||
width="700px"
|
||||
width="85%"
|
||||
@close="handleAllocateDialogClose"
|
||||
>
|
||||
<ElForm ref="allocateFormRef" :model="allocateForm" :rules="allocateRules" label-width="120px">
|
||||
<ElFormItem label="ICCID列表" prop="iccids">
|
||||
<ElInput
|
||||
v-model="iccidsText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入ICCID,每行一个或用逗号分隔"
|
||||
@input="handleIccidsChange"
|
||||
<!-- 搜索过滤条件 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="cardSearchForm"
|
||||
:items="cardSearchFormItems"
|
||||
label-width="85"
|
||||
@reset="handleCardSearchReset"
|
||||
@search="handleCardSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<!-- 卡列表 -->
|
||||
<div class="card-selection-info"> 已选择 {{ selectedAvailableCards.length }} 张卡 </div>
|
||||
<ArtTable
|
||||
ref="availableCardsTableRef"
|
||||
row-key="id"
|
||||
:loading="availableCardsLoading"
|
||||
:data="availableCardsList"
|
||||
:currentPage="cardPagination.page"
|
||||
:pageSize="cardPagination.pageSize"
|
||||
:total="cardPagination.total"
|
||||
:marginTop="10"
|
||||
@size-change="handleCardPageSizeChange"
|
||||
@current-change="handleCardPageChange"
|
||||
@selection-change="handleAvailableCardsSelectionChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn
|
||||
v-for="col in availableCardColumns"
|
||||
:key="col.prop || col.type"
|
||||
v-bind="col"
|
||||
/>
|
||||
<div style="color: var(--el-color-info); margin-top: 4px; font-size: 12px">
|
||||
已输入 {{ allocateForm.iccids?.length || 0 }} 个ICCID
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="确认设备绑定">
|
||||
<ElCheckbox v-model="allocateForm.confirm_device_bundles">
|
||||
我确认已了解设备绑定关系,同意一起授权
|
||||
</ElCheckbox>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<!-- 预检结果 -->
|
||||
<div v-if="previewData" style="margin-top: 20px">
|
||||
<ElDivider content-position="left">预检结果</ElDivider>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="待授权卡数">
|
||||
{{ previewData.summary.total_cards }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="可授权卡数">
|
||||
<ElTag type="success">{{ previewData.summary.valid_cards }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="独立卡数">
|
||||
{{ previewData.summary.standalone_cards }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备绑定数">
|
||||
<ElTag type="warning">{{ previewData.summary.device_bundles }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数" :span="2">
|
||||
<ElTag type="danger">{{ previewData.summary.failed_cards }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<!-- 失败项详情 -->
|
||||
<div v-if="previewData.failed_items && previewData.failed_items.length > 0" style="margin-top: 16px">
|
||||
<ElAlert title="以下ICCID无法授权" type="error" :closable="false" style="margin-bottom: 8px" />
|
||||
<ElTable :data="previewData.failed_items" border max-height="200">
|
||||
<ElTableColumn prop="iccid" label="ICCID" width="180" />
|
||||
<ElTableColumn prop="reason" label="失败原因" />
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<!-- 设备绑定详情 -->
|
||||
<div v-if="previewData.device_bundles && previewData.device_bundles.length > 0" style="margin-top: 16px">
|
||||
<ElAlert
|
||||
title="以下ICCID与设备绑定,授权后设备也将一起授权给企业"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
style="margin-bottom: 8px"
|
||||
/>
|
||||
<ElTable :data="previewData.device_bundles" border max-height="200">
|
||||
<ElTableColumn prop="device_imei" label="设备IMEI" width="180" />
|
||||
<ElTableColumn label="绑定卡数">
|
||||
<template #default="{ row }">
|
||||
{{ row.iccids?.length || 0 }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="ICCID列表">
|
||||
<template #default="{ row }">
|
||||
{{ row.iccids?.join(', ') }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="allocateDialogVisible = false">取消</ElButton>
|
||||
<ElButton @click="handlePreview" :loading="previewLoading">预检</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleAllocate"
|
||||
:loading="allocateLoading"
|
||||
:disabled="!previewData || previewData.summary.valid_cards === 0"
|
||||
:disabled="selectedAvailableCards.length === 0"
|
||||
>
|
||||
确认授权
|
||||
</ElButton>
|
||||
@@ -191,7 +154,12 @@
|
||||
</ElDialog>
|
||||
|
||||
<!-- 结果对话框 -->
|
||||
<ElDialog v-model="resultDialogVisible" :title="resultTitle" width="700px">
|
||||
<ElDialog
|
||||
v-model="resultDialogVisible"
|
||||
:title="resultTitle"
|
||||
width="700px"
|
||||
@close="handleResultDialogClose"
|
||||
>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<ElTag type="success">{{ operationResult.success_count }}</ElTag>
|
||||
@@ -203,7 +171,7 @@
|
||||
|
||||
<div
|
||||
v-if="operationResult.failed_items && operationResult.failed_items.length > 0"
|
||||
style="margin-top: 20px"
|
||||
class="result-section"
|
||||
>
|
||||
<ElDivider content-position="left">失败项详情</ElDivider>
|
||||
<ElTable :data="operationResult.failed_items" border max-height="300">
|
||||
@@ -215,7 +183,7 @@
|
||||
<!-- 显示授权的设备 -->
|
||||
<div
|
||||
v-if="operationResult.allocated_devices && operationResult.allocated_devices.length > 0"
|
||||
style="margin-top: 20px"
|
||||
class="result-section"
|
||||
>
|
||||
<ElDivider content-position="left">已授权设备</ElDivider>
|
||||
<ElTable :data="operationResult.allocated_devices" border max-height="200">
|
||||
@@ -242,21 +210,21 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { EnterpriseService } from '@/api/modules'
|
||||
import { EnterpriseService, CardService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { BgColorEnum } from '@/enums/appEnum'
|
||||
import type {
|
||||
EnterpriseCardItem,
|
||||
AllocateCardsPreviewResponse,
|
||||
AllocateCardsResponse,
|
||||
RecallCardsResponse,
|
||||
FailedItem
|
||||
RecallCardsResponse
|
||||
} from '@/types/api/enterpriseCard'
|
||||
import type { EnterpriseItem } from '@/types/api'
|
||||
import type { StandaloneIotCard } from '@/types/api/card'
|
||||
|
||||
defineOptions({ name: 'EnterpriseCards' })
|
||||
|
||||
@@ -265,19 +233,15 @@
|
||||
const loading = ref(false)
|
||||
const allocateDialogVisible = ref(false)
|
||||
const allocateLoading = ref(false)
|
||||
const previewLoading = ref(false)
|
||||
const recallDialogVisible = ref(false)
|
||||
const recallLoading = ref(false)
|
||||
const resultDialogVisible = ref(false)
|
||||
const resultTitle = ref('')
|
||||
const tableRef = ref()
|
||||
const allocateFormRef = ref<FormInstance>()
|
||||
const recallFormRef = ref<FormInstance>()
|
||||
const selectedCards = ref<EnterpriseCardItem[]>([])
|
||||
const enterpriseId = ref<number>(0)
|
||||
const enterpriseInfo = ref<EnterpriseItem | null>(null)
|
||||
const iccidsText = ref('')
|
||||
const previewData = ref<AllocateCardsPreviewResponse | null>(null)
|
||||
const operationResult = ref<AllocateCardsResponse | RecallCardsResponse>({
|
||||
success_count: 0,
|
||||
fail_count: 0,
|
||||
@@ -285,40 +249,39 @@
|
||||
allocated_devices: null
|
||||
})
|
||||
|
||||
// 可用卡列表相关
|
||||
const availableCardsTableRef = ref()
|
||||
const availableCardsLoading = ref(false)
|
||||
const availableCardsList = ref<StandaloneIotCard[]>([])
|
||||
const selectedAvailableCards = ref<StandaloneIotCard[]>([])
|
||||
|
||||
// 卡搜索表单初始值
|
||||
const initialCardSearchState = {
|
||||
status: undefined,
|
||||
carrier_id: undefined,
|
||||
iccid: '',
|
||||
msisdn: '',
|
||||
is_distributed: undefined
|
||||
}
|
||||
|
||||
const cardSearchForm = reactive({ ...initialCardSearchState })
|
||||
const cardPagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
iccid: '',
|
||||
msisdn: '',
|
||||
status: undefined as number | undefined,
|
||||
authorization_status: undefined as number | undefined
|
||||
device_no: '',
|
||||
carrier_id: undefined as number | undefined,
|
||||
status: undefined as number | undefined
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
// 授权表单
|
||||
const allocateForm = reactive({
|
||||
iccids: [] as string[],
|
||||
confirm_device_bundles: false
|
||||
})
|
||||
|
||||
// 授权表单验证规则
|
||||
const allocateRules = reactive<FormRules>({
|
||||
iccids: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error('请输入至少一个ICCID'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 回收表单
|
||||
const recallForm = reactive({
|
||||
reason: ''
|
||||
@@ -346,16 +309,30 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '手机号',
|
||||
prop: 'msisdn',
|
||||
label: '设备号',
|
||||
prop: 'device_no',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入手机号'
|
||||
placeholder: '请输入设备号'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '卡状态',
|
||||
label: '运营商',
|
||||
prop: 'carrier_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '中国移动', value: 1 },
|
||||
{ label: '中国联通', value: 2 },
|
||||
{ label: '中国电信', value: 3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: {
|
||||
@@ -363,21 +340,74 @@
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '激活', value: 1 },
|
||||
{ label: '停机', value: 2 }
|
||||
{ label: '在库', value: 1 },
|
||||
{ label: '已分销', value: 2 },
|
||||
{ label: '已激活', value: 3 },
|
||||
{ label: '已停用', value: 4 }
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
// 卡列表搜索表单配置
|
||||
const cardSearchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '授权状态',
|
||||
prop: 'authorization_status',
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '有效', value: 1 },
|
||||
{ label: '已回收', value: 0 }
|
||||
{ label: '在库', value: 1 },
|
||||
{ label: '已分销', value: 2 },
|
||||
{ label: '已激活', value: 3 },
|
||||
{ label: '已停用', value: 4 }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '运营商',
|
||||
prop: 'carrier_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '中国移动', value: 1 },
|
||||
{ label: '中国联通', value: 2 },
|
||||
{ label: '中国电信', value: 3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'ICCID',
|
||||
prop: 'iccid',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入ICCID'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '卡接入号',
|
||||
prop: 'msisdn',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入卡接入号'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '是否已分销',
|
||||
prop: 'is_distributed',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -385,12 +415,15 @@
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: 'ICCID', prop: 'iccid' },
|
||||
{ label: '手机号', prop: 'msisdn' },
|
||||
{ label: '卡接入号', prop: 'msisdn' },
|
||||
{ label: '设备号', prop: 'device_no' },
|
||||
{ label: '运营商ID', prop: 'carrier_id' },
|
||||
{ label: '运营商', prop: 'carrier_name' },
|
||||
{ label: '卡状态', prop: 'status' },
|
||||
{ label: '授权状态', prop: 'authorization_status' },
|
||||
{ label: '授权时间', prop: 'authorized_at' },
|
||||
{ label: '授权人', prop: 'authorizer_name' },
|
||||
{ label: '套餐名称', prop: 'package_name' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '状态名称', prop: 'status_name' },
|
||||
{ label: '网络状态', prop: 'network_status' },
|
||||
{ label: '网络状态名称', prop: 'network_status_name' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
@@ -398,22 +431,44 @@
|
||||
|
||||
// 获取卡状态标签类型
|
||||
const getCardStatusTag = (status: number) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'info'
|
||||
case 2:
|
||||
return 'warning'
|
||||
case 3:
|
||||
return 'success'
|
||||
case 4:
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取卡状态文本 - 使用API返回的status_name
|
||||
const getCardStatusText = (status: number) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '在库'
|
||||
case 2:
|
||||
return '已分销'
|
||||
case 3:
|
||||
return '已激活'
|
||||
case 4:
|
||||
return '已停用'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取网络状态标签类型
|
||||
const getNetworkStatusTag = (status: number) => {
|
||||
return status === 1 ? 'success' : 'danger'
|
||||
}
|
||||
|
||||
// 获取卡状态文本
|
||||
const getCardStatusText = (status: number) => {
|
||||
return status === 1 ? '激活' : '停机'
|
||||
}
|
||||
|
||||
// 获取授权状态标签类型
|
||||
const getAuthStatusTag = (status: number) => {
|
||||
return status === 1 ? 'success' : 'info'
|
||||
}
|
||||
|
||||
// 获取授权状态文本
|
||||
const getAuthStatusText = (status: number) => {
|
||||
return status === 1 ? '有效' : '已回收'
|
||||
// 获取网络状态文本 - 使用API返回的network_status_name
|
||||
const getNetworkStatusText = (status: number) => {
|
||||
return status === 1 ? '开机' : '停机'
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
@@ -421,64 +476,77 @@
|
||||
{
|
||||
prop: 'iccid',
|
||||
label: 'ICCID',
|
||||
minWidth: 180
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'msisdn',
|
||||
label: '手机号',
|
||||
width: 120
|
||||
label: '卡接入号',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'device_no',
|
||||
label: '设备号',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
prop: 'carrier_id',
|
||||
label: '运营商ID',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'carrier_name',
|
||||
label: '运营商',
|
||||
width: 100
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'package_name',
|
||||
label: '套餐名称',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '卡状态',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: EnterpriseCardItem) => {
|
||||
return h(ElTag, { type: getCardStatusTag(row.status) }, () => getCardStatusText(row.status))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'authorization_status',
|
||||
label: '授权状态',
|
||||
prop: 'status_name',
|
||||
label: '状态名称',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'network_status',
|
||||
label: '网络状态',
|
||||
width: 100,
|
||||
formatter: (row: EnterpriseCardItem) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: getAuthStatusTag(row.authorization_status) },
|
||||
() => getAuthStatusText(row.authorization_status)
|
||||
return h(ElTag, { type: getNetworkStatusTag(row.network_status) }, () =>
|
||||
getNetworkStatusText(row.network_status)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'authorized_at',
|
||||
label: '授权时间',
|
||||
width: 180,
|
||||
formatter: (row: EnterpriseCardItem) =>
|
||||
row.authorized_at ? formatDateTime(row.authorized_at) : '-'
|
||||
},
|
||||
{
|
||||
prop: 'authorizer_name',
|
||||
label: '授权人',
|
||||
width: 120
|
||||
prop: 'network_status_name',
|
||||
label: '网络状态名称',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
formatter: (row: EnterpriseCardItem) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
row.status === 2
|
||||
row.network_status === 0
|
||||
? h(ArtButtonTable, {
|
||||
text: '复机',
|
||||
iconClass: BgColorEnum.SUCCESS,
|
||||
onClick: () => handleResume(row)
|
||||
})
|
||||
: h(ArtButtonTable, {
|
||||
text: '停机',
|
||||
iconClass: BgColorEnum.ERROR,
|
||||
onClick: () => handleSuspend(row)
|
||||
})
|
||||
])
|
||||
@@ -522,9 +590,9 @@
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
iccid: searchForm.iccid || undefined,
|
||||
msisdn: searchForm.msisdn || undefined,
|
||||
status: searchForm.status,
|
||||
authorization_status: searchForm.authorization_status
|
||||
device_no: searchForm.device_no || undefined,
|
||||
carrier_id: searchForm.carrier_id,
|
||||
status: searchForm.status
|
||||
}
|
||||
|
||||
// 清理空值
|
||||
@@ -586,84 +654,213 @@
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 显示授权对话框
|
||||
const showAllocateDialog = () => {
|
||||
allocateDialogVisible.value = true
|
||||
iccidsText.value = ''
|
||||
allocateForm.iccids = []
|
||||
allocateForm.confirm_device_bundles = false
|
||||
previewData.value = null
|
||||
if (allocateFormRef.value) {
|
||||
allocateFormRef.value.resetFields()
|
||||
// 获取可用卡状态类型
|
||||
const getAvailableCardStatusType = (status: number) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'info'
|
||||
case 2:
|
||||
return 'warning'
|
||||
case 3:
|
||||
return 'success'
|
||||
case 4:
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 处理ICCID输入变化
|
||||
const handleIccidsChange = () => {
|
||||
// 解析输入的ICCID,支持逗号、空格、换行分隔
|
||||
const iccids = iccidsText.value
|
||||
.split(/[,\s\n]+/)
|
||||
.map((iccid) => iccid.trim())
|
||||
.filter((iccid) => iccid.length > 0)
|
||||
allocateForm.iccids = iccids
|
||||
// 清除预检结果
|
||||
previewData.value = null
|
||||
// 获取可用卡状态文本
|
||||
const getAvailableCardStatusText = (status: number) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '在库'
|
||||
case 2:
|
||||
return '已分销'
|
||||
case 3:
|
||||
return '已激活'
|
||||
case 4:
|
||||
return '已停用'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 预检
|
||||
const handlePreview = async () => {
|
||||
if (!allocateFormRef.value) return
|
||||
|
||||
await allocateFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
previewLoading.value = true
|
||||
try {
|
||||
const res = await EnterpriseService.previewAllocateCards(enterpriseId.value, {
|
||||
iccids: allocateForm.iccids
|
||||
})
|
||||
if (res.code === 0) {
|
||||
previewData.value = res.data
|
||||
ElMessage.success('预检完成')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('预检失败')
|
||||
} finally {
|
||||
previewLoading.value = false
|
||||
}
|
||||
// 可用卡列表列配置
|
||||
const availableCardColumns = computed(() => [
|
||||
{
|
||||
prop: 'iccid',
|
||||
label: 'ICCID',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'msisdn',
|
||||
label: '卡接入号',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'carrier_name',
|
||||
label: '运营商',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'cost_price',
|
||||
label: '成本价',
|
||||
width: 100,
|
||||
formatter: (row: StandaloneIotCard) => `¥${(row.cost_price / 100).toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'distribute_price',
|
||||
label: '分销价',
|
||||
width: 100,
|
||||
formatter: (row: StandaloneIotCard) => `¥${(row.distribute_price / 100).toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: StandaloneIotCard) => {
|
||||
return h(ElTag, { type: getAvailableCardStatusType(row.status) }, () =>
|
||||
getAvailableCardStatusText(row.status)
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
prop: 'activation_status',
|
||||
label: '激活状态',
|
||||
width: 100,
|
||||
formatter: (row: StandaloneIotCard) => {
|
||||
const type = row.activation_status === 1 ? 'success' : 'info'
|
||||
const text = row.activation_status === 1 ? '已激活' : '未激活'
|
||||
return h(ElTag, { type }, () => text)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'network_status',
|
||||
label: '网络状态',
|
||||
width: 100,
|
||||
formatter: (row: StandaloneIotCard) => {
|
||||
const type = row.network_status === 1 ? 'success' : 'danger'
|
||||
const text = row.network_status === 1 ? '开机' : '停机'
|
||||
return h(ElTag, { type }, () => text)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'real_name_status',
|
||||
label: '实名状态',
|
||||
width: 100,
|
||||
formatter: (row: StandaloneIotCard) => {
|
||||
const type = row.real_name_status === 1 ? 'success' : 'warning'
|
||||
const text = row.real_name_status === 1 ? '已实名' : '未实名'
|
||||
return h(ElTag, { type }, () => text)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'data_usage_mb',
|
||||
label: '累计流量(MB)',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'first_commission_paid',
|
||||
label: '首次佣金',
|
||||
width: 100,
|
||||
formatter: (row: StandaloneIotCard) => {
|
||||
const type = row.first_commission_paid ? 'success' : 'info'
|
||||
const text = row.first_commission_paid ? '已支付' : '未支付'
|
||||
return h(ElTag, { type, size: 'small' }, () => text)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'accumulated_recharge',
|
||||
label: '累计充值',
|
||||
width: 100,
|
||||
formatter: (row: StandaloneIotCard) => `¥${(row.accumulated_recharge / 100).toFixed(2)}`
|
||||
}
|
||||
])
|
||||
|
||||
// 获取可用卡列表
|
||||
const getAvailableCardsList = async () => {
|
||||
availableCardsLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: cardPagination.page,
|
||||
page_size: cardPagination.pageSize,
|
||||
...cardSearchForm
|
||||
}
|
||||
// 清理空值
|
||||
Object.keys(params).forEach((key) => {
|
||||
if (params[key] === '' || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const res = await CardService.getStandaloneIotCards(params)
|
||||
if (res.code === 0) {
|
||||
availableCardsList.value = res.data.items || []
|
||||
cardPagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取卡列表失败')
|
||||
} finally {
|
||||
availableCardsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理卡列表选择变化
|
||||
const handleAvailableCardsSelectionChange = (selection: StandaloneIotCard[]) => {
|
||||
selectedAvailableCards.value = selection
|
||||
}
|
||||
|
||||
// 卡列表搜索
|
||||
const handleCardSearch = () => {
|
||||
cardPagination.page = 1
|
||||
getAvailableCardsList()
|
||||
}
|
||||
|
||||
// 卡列表重置
|
||||
const handleCardSearchReset = () => {
|
||||
Object.assign(cardSearchForm, { ...initialCardSearchState })
|
||||
cardPagination.page = 1
|
||||
getAvailableCardsList()
|
||||
}
|
||||
|
||||
// 卡列表分页大小变化
|
||||
const handleCardPageSizeChange = (newPageSize: number) => {
|
||||
cardPagination.pageSize = newPageSize
|
||||
getAvailableCardsList()
|
||||
}
|
||||
|
||||
// 卡列表页码变化
|
||||
const handleCardPageChange = (newPage: number) => {
|
||||
cardPagination.page = newPage
|
||||
getAvailableCardsList()
|
||||
}
|
||||
|
||||
// 显示授权对话框
|
||||
const showAllocateDialog = () => {
|
||||
allocateDialogVisible.value = true
|
||||
selectedAvailableCards.value = []
|
||||
// 重置搜索条件
|
||||
Object.assign(cardSearchForm, { ...initialCardSearchState })
|
||||
cardPagination.page = 1
|
||||
cardPagination.pageSize = 20
|
||||
// 加载可用卡列表
|
||||
getAvailableCardsList()
|
||||
}
|
||||
|
||||
// 执行授权
|
||||
const handleAllocate = async () => {
|
||||
if (!allocateFormRef.value) return
|
||||
|
||||
if (!previewData.value) {
|
||||
ElMessage.warning('请先进行预检')
|
||||
return
|
||||
}
|
||||
|
||||
if (previewData.value.summary.valid_cards === 0) {
|
||||
ElMessage.warning('没有可授权的卡')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果有设备绑定且未确认,提示用户
|
||||
if (
|
||||
previewData.value.device_bundles &&
|
||||
previewData.value.device_bundles.length > 0 &&
|
||||
!allocateForm.confirm_device_bundles
|
||||
) {
|
||||
ElMessage.warning('请确认设备绑定关系')
|
||||
if (selectedAvailableCards.value.length === 0) {
|
||||
ElMessage.warning('请选择要授权的卡')
|
||||
return
|
||||
}
|
||||
|
||||
allocateLoading.value = true
|
||||
try {
|
||||
const iccids = selectedAvailableCards.value.map((card) => card.iccid)
|
||||
const res = await EnterpriseService.allocateCards(enterpriseId.value, {
|
||||
iccids: allocateForm.iccids,
|
||||
confirm_device_bundles: allocateForm.confirm_device_bundles || undefined
|
||||
iccids
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
@@ -683,10 +880,7 @@
|
||||
|
||||
// 关闭授权对话框
|
||||
const handleAllocateDialogClose = () => {
|
||||
if (allocateFormRef.value) {
|
||||
allocateFormRef.value.resetFields()
|
||||
}
|
||||
previewData.value = null
|
||||
selectedAvailableCards.value = []
|
||||
}
|
||||
|
||||
// 显示批量回收对话框
|
||||
@@ -711,7 +905,7 @@
|
||||
recallLoading.value = true
|
||||
try {
|
||||
const res = await EnterpriseService.recallCards(enterpriseId.value, {
|
||||
card_ids: selectedCards.value.map((card) => card.id)
|
||||
iccids: selectedCards.value.map((card) => card.iccid)
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
@@ -743,6 +937,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭结果对话框
|
||||
const handleResultDialogClose = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 停机卡
|
||||
const handleSuspend = (row: EnterpriseCardItem) => {
|
||||
ElMessageBox.confirm('确定要停机该卡吗?', '停机卡', {
|
||||
@@ -786,10 +985,34 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.enterprise-cards-page {
|
||||
.enterprise-info-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-selection-info {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--el-color-info);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.card-pagination {
|
||||
margin-top: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { BgColorEnum } from '@/enums/appEnum'
|
||||
|
||||
defineOptions({ name: 'EnterpriseCustomer' })
|
||||
|
||||
@@ -367,7 +368,8 @@
|
||||
{
|
||||
prop: 'enterprise_code',
|
||||
label: '企业编号',
|
||||
minWidth: 150
|
||||
minWidth: 150,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'enterprise_name',
|
||||
@@ -423,7 +425,8 @@
|
||||
activeText: '启用',
|
||||
inactiveText: '禁用',
|
||||
inlinePrompt: true,
|
||||
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
|
||||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||
handleStatusChange(row, val as number)
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -436,21 +439,24 @@
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 230,
|
||||
width: 260,
|
||||
fixed: 'right',
|
||||
formatter: (row: EnterpriseItem) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
h(ArtButtonTable, {
|
||||
icon: '',
|
||||
text: '编辑',
|
||||
iconClass: BgColorEnum.SECONDARY,
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '卡授权',
|
||||
iconClass: BgColorEnum.PRIMARY,
|
||||
onClick: () => manageCards(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
icon: '',
|
||||
text: '修改密码',
|
||||
iconClass: BgColorEnum.WARNING,
|
||||
onClick: () => showPasswordDialog(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
@@ -367,7 +367,8 @@
|
||||
activeText: getStatusText(CommonStatus.ENABLED),
|
||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||
inlinePrompt: true,
|
||||
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
|
||||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||
handleStatusChange(row, val as number)
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -447,8 +448,11 @@
|
||||
currentAccountId.value = row.ID
|
||||
selectedRoles.value = []
|
||||
|
||||
// 先加载当前账号的角色,再打开对话框
|
||||
try {
|
||||
// 每次打开对话框时重新加载最新的角色列表
|
||||
await loadAllRoles()
|
||||
|
||||
// 先加载当前账号的角色,再打开对话框
|
||||
const res = await PlatformAccountService.getPlatformAccountRoles(row.ID)
|
||||
if (res.code === 0) {
|
||||
// 提取角色ID数组
|
||||
@@ -471,6 +475,8 @@
|
||||
})
|
||||
ElMessage.success('分配角色成功')
|
||||
roleDialogVisible.value = false
|
||||
// 刷新列表以更新角色显示
|
||||
await getAccountList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
|
||||
@@ -300,15 +300,15 @@
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'id',
|
||||
label: 'ID',
|
||||
label: 'ID'
|
||||
},
|
||||
{
|
||||
prop: 'username',
|
||||
label: '用户名',
|
||||
label: '用户名'
|
||||
},
|
||||
{
|
||||
prop: 'phone',
|
||||
label: '手机号',
|
||||
label: '手机号'
|
||||
},
|
||||
{
|
||||
prop: 'shop_name',
|
||||
@@ -326,7 +326,8 @@
|
||||
activeText: getStatusText(CommonStatus.ENABLED),
|
||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||
inlinePrompt: true,
|
||||
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
|
||||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||
handleStatusChange(row, val as number)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
<ElSkeleton :loading="loading" :rows="10" animated>
|
||||
<template #default>
|
||||
<ElDescriptions v-if="recordDetail" title="基本信息" :column="3" border>
|
||||
<ElDescriptionsItem label="分配单号">{{ recordDetail.allocation_no }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="分配单号">{{
|
||||
recordDetail.allocation_no
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="分配类型">
|
||||
<ElTag :type="getAllocationTypeType(recordDetail.allocation_type)">
|
||||
{{ recordDetail.allocation_name }}
|
||||
@@ -23,14 +25,24 @@
|
||||
{{ recordDetail.asset_type_name }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="资产标识符">{{ recordDetail.asset_identifier }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="关联卡数量">{{ recordDetail.related_card_count }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="资产标识符">{{
|
||||
recordDetail.asset_identifier
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="关联卡数量">{{
|
||||
recordDetail.related_card_count
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="关联设备ID">
|
||||
{{ recordDetail.related_device_id || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDescriptions v-if="recordDetail" title="所有者信息" :column="2" border style="margin-top: 20px">
|
||||
<ElDescriptions
|
||||
v-if="recordDetail"
|
||||
title="所有者信息"
|
||||
:column="2"
|
||||
border
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<ElDescriptionsItem label="来源所有者">
|
||||
{{ recordDetail.from_owner_name }} ({{ recordDetail.from_owner_type }})
|
||||
</ElDescriptionsItem>
|
||||
@@ -39,8 +51,16 @@
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDescriptions v-if="recordDetail" title="操作信息" :column="2" border style="margin-top: 20px">
|
||||
<ElDescriptionsItem label="操作人">{{ recordDetail.operator_name }}</ElDescriptionsItem>
|
||||
<ElDescriptions
|
||||
v-if="recordDetail"
|
||||
title="操作信息"
|
||||
:column="2"
|
||||
border
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<ElDescriptionsItem label="操作人">{{
|
||||
recordDetail.operator_name
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">
|
||||
{{ formatDateTime(recordDetail.created_at) }}
|
||||
</ElDescriptionsItem>
|
||||
@@ -50,7 +70,14 @@
|
||||
</ElDescriptions>
|
||||
|
||||
<!-- 关联卡列表 -->
|
||||
<div v-if="recordDetail && recordDetail.related_card_ids && recordDetail.related_card_ids.length > 0" style="margin-top: 20px">
|
||||
<div
|
||||
v-if="
|
||||
recordDetail &&
|
||||
recordDetail.related_card_ids &&
|
||||
recordDetail.related_card_ids.length > 0
|
||||
"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<ElDivider content-position="left">关联卡列表</ElDivider>
|
||||
<ElTable :data="relatedCardsList" border>
|
||||
<ElTableColumn type="index" label="序号" width="60" />
|
||||
@@ -155,8 +182,8 @@
|
||||
.allocation-record-detail-page {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,11 +53,7 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type {
|
||||
AssetAllocationRecord,
|
||||
AllocationTypeEnum,
|
||||
AssetTypeEnum
|
||||
} from '@/types/api/card'
|
||||
import type { AssetAllocationRecord, AllocationTypeEnum, AssetTypeEnum } from '@/types/api/card'
|
||||
|
||||
defineOptions({ name: 'AssetAllocationRecords' })
|
||||
|
||||
|
||||
@@ -46,7 +46,9 @@
|
||||
{{ authorizationDetail.revoker_name || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="回收时间">
|
||||
{{ authorizationDetail.revoked_at ? formatDateTime(authorizationDetail.revoked_at) : '-' }}
|
||||
{{
|
||||
authorizationDetail.revoked_at ? formatDateTime(authorizationDetail.revoked_at) : '-'
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="备注" :span="2">
|
||||
@@ -109,8 +111,8 @@
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -240,10 +240,8 @@
|
||||
label: '授权人类型',
|
||||
width: 100,
|
||||
formatter: (row: AuthorizationItem) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: getAuthorizerTypeTag(row.authorizer_type) },
|
||||
() => getAuthorizerTypeText(row.authorizer_type)
|
||||
return h(ElTag, { type: getAuthorizerTypeTag(row.authorizer_type) }, () =>
|
||||
getAuthorizerTypeText(row.authorizer_type)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<ElDescriptionsItem label="制造商">{{ deviceInfo.manufacturer }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最大插槽数">{{ deviceInfo.max_sim_slots }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已绑定卡数">
|
||||
<span style="color: #67c23a; font-weight: bold">{{ deviceInfo.bound_card_count }}</span>
|
||||
<span style="font-weight: bold; color: #67c23a">{{ deviceInfo.bound_card_count }}</span>
|
||||
/ {{ deviceInfo.max_sim_slots }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="所属店铺">
|
||||
@@ -118,7 +118,11 @@
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="插槽位置" prop="slot_position">
|
||||
<ElSelect v-model="bindForm.slot_position" placeholder="请选择插槽位置" style="width: 100%">
|
||||
<ElSelect
|
||||
v-model="bindForm.slot_position"
|
||||
placeholder="请选择插槽位置"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="slot in availableSlots"
|
||||
:key="slot"
|
||||
|
||||
@@ -56,9 +56,14 @@
|
||||
|
||||
<!-- 批量分配对话框 -->
|
||||
<ElDialog v-model="allocateDialogVisible" title="批量分配设备" width="600px">
|
||||
<ElForm ref="allocateFormRef" :model="allocateForm" :rules="allocateRules" label-width="120px">
|
||||
<ElForm
|
||||
ref="allocateFormRef"
|
||||
:model="allocateForm"
|
||||
:rules="allocateRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="已选设备数">
|
||||
<span style="color: #409eff; font-weight: bold">{{ selectedDevices.length }}</span> 台
|
||||
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span> 台
|
||||
</ElFormItem>
|
||||
<ElFormItem label="目标店铺" prop="target_shop_id">
|
||||
<ElSelect
|
||||
@@ -93,7 +98,8 @@
|
||||
style="margin-bottom: 10px"
|
||||
>
|
||||
<template #title>
|
||||
成功分配 {{ allocateResult.success_count }} 台,失败 {{ allocateResult.fail_count }} 台
|
||||
成功分配 {{ allocateResult.success_count }} 台,失败
|
||||
{{ allocateResult.fail_count }} 台
|
||||
</template>
|
||||
</ElAlert>
|
||||
<div v-if="allocateResult.failed_items && allocateResult.failed_items.length > 0">
|
||||
@@ -101,7 +107,7 @@
|
||||
<div
|
||||
v-for="item in allocateResult.failed_items"
|
||||
:key="item.device_id"
|
||||
style="margin-bottom: 8px; color: #f56c6c; font-size: 12px"
|
||||
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c"
|
||||
>
|
||||
设备号: {{ item.device_no }} - {{ item.reason }}
|
||||
</div>
|
||||
@@ -129,7 +135,7 @@
|
||||
<ElDialog v-model="recallDialogVisible" title="批量回收设备" width="600px">
|
||||
<ElForm ref="recallFormRef" :model="recallForm" label-width="120px">
|
||||
<ElFormItem label="已选设备数">
|
||||
<span style="color: #e6a23c; font-weight: bold">{{ selectedDevices.length }}</span> 台
|
||||
<span style="font-weight: bold; color: #e6a23c">{{ selectedDevices.length }}</span> 台
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注">
|
||||
<ElInput
|
||||
@@ -157,7 +163,7 @@
|
||||
<div
|
||||
v-for="item in recallResult.failed_items"
|
||||
:key="item.device_id"
|
||||
style="margin-bottom: 8px; color: #f56c6c; font-size: 12px"
|
||||
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c"
|
||||
>
|
||||
设备号: {{ item.device_no }} - {{ item.reason }}
|
||||
</div>
|
||||
@@ -182,10 +188,19 @@
|
||||
</ElDialog>
|
||||
|
||||
<!-- 批量设置套餐系列绑定对话框 -->
|
||||
<ElDialog v-model="seriesBindingDialogVisible" title="批量设置设备套餐系列绑定" width="600px">
|
||||
<ElForm ref="seriesBindingFormRef" :model="seriesBindingForm" :rules="seriesBindingRules" label-width="120px">
|
||||
<ElDialog
|
||||
v-model="seriesBindingDialogVisible"
|
||||
title="批量设置设备套餐系列绑定"
|
||||
width="600px"
|
||||
>
|
||||
<ElForm
|
||||
ref="seriesBindingFormRef"
|
||||
:model="seriesBindingForm"
|
||||
:rules="seriesBindingRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="已选设备数">
|
||||
<span style="color: #409eff; font-weight: bold">{{ selectedDevices.length }}</span> 台
|
||||
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span> 台
|
||||
</ElFormItem>
|
||||
<ElFormItem label="套餐系列分配" prop="series_allocation_id">
|
||||
<ElSelect
|
||||
@@ -214,15 +229,18 @@
|
||||
style="margin-bottom: 10px"
|
||||
>
|
||||
<template #title>
|
||||
成功设置 {{ seriesBindingResult.success_count }} 台,失败 {{ seriesBindingResult.fail_count }} 台
|
||||
成功设置 {{ seriesBindingResult.success_count }} 台,失败
|
||||
{{ seriesBindingResult.fail_count }} 台
|
||||
</template>
|
||||
</ElAlert>
|
||||
<div v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0">
|
||||
<div
|
||||
v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0"
|
||||
>
|
||||
<div style="margin-bottom: 10px; font-weight: bold">失败详情:</div>
|
||||
<div
|
||||
v-for="item in seriesBindingResult.failed_items"
|
||||
:key="item.device_id"
|
||||
style="margin-bottom: 8px; color: #f56c6c; font-size: 12px"
|
||||
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c"
|
||||
>
|
||||
设备号: {{ item.device_no }} - {{ item.reason }}
|
||||
</div>
|
||||
@@ -245,6 +263,61 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 设备详情弹窗 -->
|
||||
<ElDialog v-model="deviceDetailDialogVisible" title="设备详情" width="900px">
|
||||
<div v-if="deviceDetailLoading" style="text-align: center; padding: 40px 0">
|
||||
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||
</div>
|
||||
<ElDescriptions v-else-if="currentDeviceDetail" :column="3" border>
|
||||
<ElDescriptionsItem label="设备ID">{{ currentDeviceDetail.id }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备号" :span="2">{{
|
||||
currentDeviceDetail.device_no
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="设备名称">{{
|
||||
currentDeviceDetail.device_name || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备型号">{{
|
||||
currentDeviceDetail.device_model || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备类型">{{
|
||||
currentDeviceDetail.device_type || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="制造商">{{
|
||||
currentDeviceDetail.manufacturer || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最大插槽数">{{
|
||||
currentDeviceDetail.max_sim_slots
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已绑定卡数量">{{
|
||||
currentDeviceDetail.bound_card_count
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="getDeviceStatusTagType(currentDeviceDetail.status)">
|
||||
{{ currentDeviceDetail.status_name }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="店铺名称">{{
|
||||
currentDeviceDetail.shop_name || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="批次号">{{
|
||||
currentDeviceDetail.batch_no || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="激活时间">{{
|
||||
currentDeviceDetail.activated_at || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{
|
||||
currentDeviceDetail.created_at || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新时间">{{
|
||||
currentDeviceDetail.updated_at || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -255,7 +328,8 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
import { DeviceService, ShopService } from '@/api/modules'
|
||||
import { ShopSeriesAllocationService } from '@/api/modules/shopSeriesAllocation'
|
||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElIcon } from 'element-plus'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
Device,
|
||||
@@ -301,6 +375,11 @@
|
||||
})
|
||||
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
|
||||
|
||||
// 设备详情弹窗相关
|
||||
const deviceDetailDialogVisible = ref(false)
|
||||
const deviceDetailLoading = ref(false)
|
||||
const currentDeviceDetail = ref<any>(null)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
device_no: '',
|
||||
@@ -424,6 +503,40 @@
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 查看设备详情(通过弹窗)
|
||||
const goToDeviceSearchDetail = async (deviceNo: string) => {
|
||||
deviceDetailDialogVisible.value = true
|
||||
deviceDetailLoading.value = true
|
||||
currentDeviceDetail.value = null
|
||||
|
||||
try {
|
||||
const res = await DeviceService.getDeviceByImei(deviceNo)
|
||||
if (res.code === 0 && res.data) {
|
||||
currentDeviceDetail.value = res.data
|
||||
} else {
|
||||
ElMessage.error(res.message || '查询失败')
|
||||
deviceDetailDialogVisible.value = false
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询设备详情失败:', error)
|
||||
ElMessage.error(error?.message || '查询失败,请检查设备号是否正确')
|
||||
deviceDetailDialogVisible.value = false
|
||||
} finally {
|
||||
deviceDetailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备状态标签类型
|
||||
const getDeviceStatusTagType = (status: number) => {
|
||||
const typeMap: Record<number, any> = {
|
||||
1: 'info', // 在库
|
||||
2: 'warning', // 已分销
|
||||
3: 'success', // 已激活
|
||||
4: 'danger' // 已停用
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
@@ -434,7 +547,17 @@
|
||||
{
|
||||
prop: 'device_no',
|
||||
label: '设备号',
|
||||
minWidth: 150
|
||||
minWidth: 150,
|
||||
formatter: (row: Device) => {
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
style: { color: 'var(--el-color-primary)', cursor: 'pointer' },
|
||||
onClick: () => goToDeviceSearchDetail(row.device_no)
|
||||
},
|
||||
row.device_no
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'device_name',
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton type="primary" :loading="loading" @click="handleSearch">
|
||||
查询
|
||||
</ElButton>
|
||||
<ElButton type="primary" :loading="loading" @click="handleSearch"> 查询 </ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
@@ -38,25 +36,41 @@
|
||||
|
||||
<ElDescriptions :column="3" border>
|
||||
<ElDescriptionsItem label="设备ID">{{ deviceDetail.id }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备号" :span="2">{{ deviceDetail.device_no }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备号" :span="2">{{
|
||||
deviceDetail.device_no
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="设备名称">{{ deviceDetail.device_name || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备型号">{{ deviceDetail.device_model || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备类型">{{ deviceDetail.device_type || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备名称">{{
|
||||
deviceDetail.device_name || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备型号">{{
|
||||
deviceDetail.device_model || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备类型">{{
|
||||
deviceDetail.device_type || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="制造商">{{ deviceDetail.manufacturer || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="制造商">{{
|
||||
deviceDetail.manufacturer || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最大插槽数">{{ deviceDetail.max_sim_slots }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已绑定卡数量">{{ deviceDetail.bound_card_count }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已绑定卡数量">{{
|
||||
deviceDetail.bound_card_count
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="getStatusTagType(deviceDetail.status)">
|
||||
{{ deviceDetail.status_name }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="店铺名称">{{ deviceDetail.shop_name || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="店铺名称">{{
|
||||
deviceDetail.shop_name || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="批次号">{{ deviceDetail.batch_no }}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="激活时间">{{ deviceDetail.activated_at || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="激活时间">{{
|
||||
deviceDetail.activated_at || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{ deviceDetail.created_at }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新时间">{{ deviceDetail.updated_at }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
@@ -15,7 +16,13 @@
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" :icon="Upload" @click="importDialogVisible = true">
|
||||
批量导入设备
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
@@ -36,25 +43,149 @@
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 导入对话框 -->
|
||||
<ElDialog v-model="importDialogVisible" title="批量导入设备" width="700px" 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. 请先下载 CSV 模板文件,按照模板格式填写设备信息</p>
|
||||
<p>2. 支持 CSV 格式(.csv),单次最多导入 1000 条</p>
|
||||
<p>3. CSV 文件编码:UTF-8(推荐)或 GBK</p>
|
||||
<p
|
||||
>4.
|
||||
必填字段:device_no(设备号)、device_name(设备名称)、device_model(设备型号)</p
|
||||
>
|
||||
<p
|
||||
>5.
|
||||
可选字段:device_type(设备类型)、manufacturer(制造商)、max_sim_slots(最大插槽数,默认1)</p
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<div style="margin-bottom: 20px">
|
||||
<ElButton type="primary" :icon="Download" @click="downloadTemplate">
|
||||
下载导入模板
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:limit="1"
|
||||
accept=".csv"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将 CSV 文件拖到此处,或<em>点击选择</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">只能上传 CSV 文件,且不超过 10MB</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="handleCancelImport">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="uploading"
|
||||
:disabled="!fileList.length"
|
||||
@click="submitUpload"
|
||||
>
|
||||
开始导入
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 任务详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="设备导入任务详情" width="900px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="任务编号" :span="2">{{
|
||||
currentDetail.task_no
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="getStatusType(currentDetail.status)">{{ currentDetail.status_text }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="批次号">{{ currentDetail.batch_no }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="文件名" :span="2">{{
|
||||
currentDetail.file_name
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="总数">{{ currentDetail.total_count }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.success_count }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="跳过数">{{ currentDetail.skip_count }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<span style="color: var(--el-color-danger)">{{ currentDetail.fail_count }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="警告数">
|
||||
<span style="color: var(--el-color-warning)">{{ currentDetail.warning_count }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="开始时间">{{ currentDetail.started_at }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="完成时间">{{ currentDetail.completed_at }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{ currentDetail.created_at }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="错误信息" :span="2">{{
|
||||
currentDetail.error_message
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDivider content-position="left">失败明细</ElDivider>
|
||||
<div
|
||||
v-if="currentDetail.failed_items && currentDetail.failed_items.length"
|
||||
style="max-height: 300px; overflow-y: auto"
|
||||
>
|
||||
<ElTable :data="currentDetail.failed_items" border size="small">
|
||||
<ElTableColumn label="行号" type="index" width="80" :index="(index) => index + 1" />
|
||||
<ElTableColumn label="设备编号" prop="device_no" width="150" />
|
||||
<ElTableColumn label="ICCID" prop="iccid" width="200" />
|
||||
<ElTableColumn label="失败原因" prop="reason" min-width="200">
|
||||
<template #default="{ row }">
|
||||
{{ row.reason || row.error || '未知错误' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
<ElEmpty v-else description="无失败记录" />
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
<ElButton
|
||||
v-if="currentDetail.fail_count > 0"
|
||||
type="primary"
|
||||
:icon="Download"
|
||||
@click="downloadFailData"
|
||||
>
|
||||
下载失败数据
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { DeviceService } from '@/api/modules'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
|
||||
import type { UploadInstance } from 'element-plus'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { StorageService } from '@/api/modules/storage'
|
||||
import type { DeviceImportTask, DeviceImportTaskStatus } from '@/types/api/device'
|
||||
|
||||
defineOptions({ name: 'DeviceTask' })
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const fileList = ref<File[]>([])
|
||||
const uploading = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
@@ -116,19 +247,21 @@
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '任务编号', prop: 'task_no' },
|
||||
{ label: '批次号', prop: 'batch_no' },
|
||||
{ label: '文件名', prop: 'file_name' },
|
||||
{ label: '任务状态', prop: 'status' },
|
||||
{ label: '文件名', prop: 'file_name' },
|
||||
{ label: '总数', prop: 'total_count' },
|
||||
{ label: '成功数', prop: 'success_count' },
|
||||
{ label: '失败数', prop: 'fail_count' },
|
||||
{ label: '跳过数', prop: 'skip_count' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '开始时间', prop: 'started_at' },
|
||||
{ label: '完成时间', prop: 'completed_at' },
|
||||
{ label: '错误信息', prop: 'error_message' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
const taskList = ref<DeviceImportTask[]>([])
|
||||
const currentDetail = ref<any>({})
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: DeviceImportTaskStatus) => {
|
||||
@@ -147,14 +280,22 @@
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (row: DeviceImportTask) => {
|
||||
router.push({
|
||||
path: '/asset-management/task-detail',
|
||||
query: {
|
||||
id: row.id,
|
||||
task_type: 'device'
|
||||
const viewDetail = async (row: DeviceImportTask) => {
|
||||
try {
|
||||
const res = await DeviceService.getImportTaskDetail(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
currentDetail.value = {
|
||||
...res.data,
|
||||
started_at: res.data.started_at ? formatDateTime(res.data.started_at) : '-',
|
||||
completed_at: res.data.completed_at ? formatDateTime(res.data.completed_at) : '-',
|
||||
created_at: res.data.created_at ? formatDateTime(res.data.created_at) : '-'
|
||||
}
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error)
|
||||
ElMessage.error('获取任务详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
@@ -162,17 +303,7 @@
|
||||
{
|
||||
prop: 'task_no',
|
||||
label: '任务编号',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
prop: 'batch_no',
|
||||
label: '批次号',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'file_name',
|
||||
label: '文件名',
|
||||
minWidth: 200
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
@@ -182,6 +313,12 @@
|
||||
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_text)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'file_name',
|
||||
label: '文件名',
|
||||
minWidth: 250,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'total_count',
|
||||
label: '总数',
|
||||
@@ -190,15 +327,17 @@
|
||||
{
|
||||
prop: 'success_count',
|
||||
label: '成功数',
|
||||
width: 80
|
||||
width: 80,
|
||||
formatter: (row: DeviceImportTask) => {
|
||||
return h('span', { style: { color: 'var(--el-color-success)' } }, row.success_count)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'fail_count',
|
||||
label: '失败数',
|
||||
width: 80,
|
||||
formatter: (row: DeviceImportTask) => {
|
||||
const type = row.fail_count > 0 ? 'danger' : 'success'
|
||||
return h(ElTag, { type, size: 'small' }, () => row.fail_count)
|
||||
return h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -207,27 +346,59 @@
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 160,
|
||||
formatter: (row: DeviceImportTask) => formatDateTime(row.created_at)
|
||||
prop: 'started_at',
|
||||
label: '开始时间',
|
||||
width: 180,
|
||||
formatter: (row: DeviceImportTask) => (row.started_at ? formatDateTime(row.started_at) : '-')
|
||||
},
|
||||
{
|
||||
prop: 'completed_at',
|
||||
label: '完成时间',
|
||||
width: 160,
|
||||
formatter: (row: DeviceImportTask) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
|
||||
width: 180,
|
||||
formatter: (row: DeviceImportTask) =>
|
||||
row.completed_at ? formatDateTime(row.completed_at) : '-'
|
||||
},
|
||||
{
|
||||
prop: 'error_message',
|
||||
label: '错误信息',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: DeviceImportTask) => row.error_message || '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: DeviceImportTask) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 100,
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
formatter: (row: DeviceImportTask) => {
|
||||
return h(ArtButtonTable, {
|
||||
type: 'view',
|
||||
onClick: () => viewDetail(row)
|
||||
})
|
||||
const buttons = []
|
||||
|
||||
// 显示"查看详情"按钮
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '详情',
|
||||
onClick: () => viewDetail(row)
|
||||
})
|
||||
)
|
||||
|
||||
// 如果有失败数据,显示"失败数据"按钮
|
||||
if (row.fail_count > 0) {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '失败数据',
|
||||
type: 'danger',
|
||||
onClick: () => downloadFailDataByRow(row)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||
}
|
||||
}
|
||||
])
|
||||
@@ -262,7 +433,7 @@
|
||||
|
||||
const res = await DeviceService.getImportTasks(params)
|
||||
if (res.code === 0) {
|
||||
taskList.value = res.data.list || []
|
||||
taskList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -301,10 +472,195 @@
|
||||
pagination.page = newCurrentPage
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
const csvContent = [
|
||||
'device_no,device_name,device_model,device_type,manufacturer,max_sim_slots',
|
||||
'DEV001,智能水表01,WM-2000,智能水表,华为,1',
|
||||
'DEV002,GPS定位器01,GPS-3000,定位设备,小米,2',
|
||||
'DEV003,智能燃气表01,GM-1500,智能燃气表,海尔,1'
|
||||
].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', '设备导入模板.csv')
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('设备导入模板下载成功')
|
||||
}
|
||||
|
||||
// 文件选择变化
|
||||
const handleFileChange = (uploadFile: any) => {
|
||||
const maxSize = 10 * 1024 * 1024
|
||||
if (uploadFile.raw && uploadFile.raw.size > maxSize) {
|
||||
ElMessage.error('文件大小不能超过 10MB')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.csv')) {
|
||||
ElMessage.error('只能上传 CSV 文件')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
fileList.value = uploadFile.raw ? [uploadFile.raw] : []
|
||||
}
|
||||
|
||||
// 清空文件
|
||||
const clearFiles = () => {
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
}
|
||||
|
||||
// 取消导入
|
||||
const handleCancelImport = () => {
|
||||
clearFiles()
|
||||
importDialogVisible.value = false
|
||||
}
|
||||
|
||||
// 提交上传
|
||||
const submitUpload = async () => {
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择CSV文件')
|
||||
return
|
||||
}
|
||||
|
||||
const file = fileList.value[0]
|
||||
uploading.value = true
|
||||
|
||||
try {
|
||||
ElMessage.info('正在准备上传...')
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: 'text/csv',
|
||||
purpose: 'iot_import'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
|
||||
ElMessage.info('正在上传文件...')
|
||||
await StorageService.uploadFile(upload_url, file, 'text/csv')
|
||||
|
||||
ElMessage.info('正在创建导入任务...')
|
||||
const importRes = await DeviceService.importDevices({
|
||||
file_key,
|
||||
batch_no: `DEV-${Date.now()}`
|
||||
})
|
||||
|
||||
if (importRes.code !== 0) {
|
||||
throw new Error(importRes.msg || '创建导入任务失败')
|
||||
}
|
||||
|
||||
const taskNo = importRes.data.task_no
|
||||
|
||||
handleCancelImport()
|
||||
getTableData()
|
||||
|
||||
ElMessage.success({
|
||||
message: `导入任务已创建!任务编号:${taskNo}`,
|
||||
duration: 3000,
|
||||
showClose: true
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('设备导入失败:', error)
|
||||
ElMessage.error(error.message || '设备导入失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 从行数据下载失败数据
|
||||
const downloadFailDataByRow = async (row: DeviceImportTask) => {
|
||||
try {
|
||||
const res = await DeviceService.getImportTaskDetail(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
const detail = res.data
|
||||
downloadFailDataFromDetail(detail, row.batch_no)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败数据失败:', error)
|
||||
ElMessage.error('下载失败数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 下载失败数据(从详情对话框)
|
||||
const downloadFailData = () => {
|
||||
downloadFailDataFromDetail(currentDetail.value, currentDetail.value.batch_no)
|
||||
}
|
||||
|
||||
// 下载失败数据的通用方法
|
||||
const downloadFailDataFromDetail = (detail: any, batchNo: string) => {
|
||||
const failReasons =
|
||||
detail.failed_items?.map((item: any, index: number) => ({
|
||||
row: index + 1,
|
||||
deviceCode: item.device_no || '-',
|
||||
iccid: item.iccid || '-',
|
||||
message: item.reason || item.error || '未知错误'
|
||||
})) || []
|
||||
|
||||
if (failReasons.length === 0) {
|
||||
ElMessage.warning('没有失败数据可下载')
|
||||
return
|
||||
}
|
||||
|
||||
const headers = ['行号', '设备编号', 'ICCID', '失败原因']
|
||||
const csvRows = [
|
||||
headers.join(','),
|
||||
...failReasons.map((item: any) =>
|
||||
[item.row, item.deviceCode, item.iccid, `"${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', `导入失败数据_${batchNo}.csv`)
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('失败数据下载成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device-task-page {
|
||||
// Device task page styles
|
||||
:deep(.el-icon--upload) {
|
||||
margin-bottom: 16px;
|
||||
font-size: 67px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
:deep(.el-upload__text) {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
:placeholder="$t('enterpriseDevices.form.deviceNosPlaceholder')"
|
||||
@input="handleDeviceNosChange"
|
||||
/>
|
||||
<div style="color: var(--el-color-info); margin-top: 4px; font-size: 12px">
|
||||
<div style="margin-top: 4px; font-size: 12px; color: var(--el-color-info)">
|
||||
{{
|
||||
$t('enterpriseDevices.form.selectedCount', {
|
||||
count: allocateForm.device_nos?.length || 0
|
||||
@@ -110,9 +110,7 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="allocateDialogVisible = false">{{
|
||||
$t('common.cancel')
|
||||
}}</ElButton>
|
||||
<ElButton @click="allocateDialogVisible = false">{{ $t('common.cancel') }}</ElButton>
|
||||
<ElButton type="primary" @click="handleAllocate" :loading="allocateLoading">
|
||||
{{ $t('common.confirm') }}
|
||||
</ElButton>
|
||||
@@ -635,8 +633,8 @@
|
||||
.enterprise-devices-page {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,13 +19,25 @@
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showImportDialog">导入ICCID</ElButton>
|
||||
<ElButton type="success" :disabled="selectedCards.length === 0" @click="showAllocateDialog">
|
||||
<ElButton
|
||||
type="success"
|
||||
:disabled="selectedCards.length === 0"
|
||||
@click="showAllocateDialog"
|
||||
>
|
||||
批量分配
|
||||
</ElButton>
|
||||
<ElButton type="warning" :disabled="selectedCards.length === 0" @click="showRecallDialog">
|
||||
<ElButton
|
||||
type="warning"
|
||||
:disabled="selectedCards.length === 0"
|
||||
@click="showRecallDialog"
|
||||
>
|
||||
批量回收
|
||||
</ElButton>
|
||||
<ElButton type="info" :disabled="selectedCards.length === 0" @click="showSeriesBindingDialog">
|
||||
<ElButton
|
||||
type="info"
|
||||
:disabled="selectedCards.length === 0"
|
||||
@click="showSeriesBindingDialog"
|
||||
>
|
||||
批量设置套餐系列
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="cardDistribution">网卡分销</ElButton>
|
||||
@@ -65,7 +77,11 @@
|
||||
>
|
||||
<ElForm ref="importFormRef" :model="importForm" :rules="importRules" label-width="100px">
|
||||
<ElFormItem label="运营商" prop="carrier_id">
|
||||
<ElSelect v-model="importForm.carrier_id" placeholder="请选择运营商" style="width: 100%">
|
||||
<ElSelect
|
||||
v-model="importForm.carrier_id"
|
||||
placeholder="请选择运营商"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="中国移动" :value="1" />
|
||||
<ElOption label="中国联通" :value="2" />
|
||||
<ElOption label="中国电信" :value="3" />
|
||||
@@ -91,7 +107,7 @@
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
<div>只支持上传CSV文件,且不超过10MB</div>
|
||||
<div style="color: var(--el-color-info); margin-top: 4px">
|
||||
<div style="margin-top: 4px; color: var(--el-color-info)">
|
||||
CSV格式:ICCID,MSISDN(两列,逗号分隔,每行一条记录)
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,9 +132,18 @@
|
||||
width="600px"
|
||||
@close="handleAllocateDialogClose"
|
||||
>
|
||||
<ElForm ref="allocateFormRef" :model="allocateForm" :rules="allocateRules" label-width="120px">
|
||||
<ElForm
|
||||
ref="allocateFormRef"
|
||||
:model="allocateForm"
|
||||
:rules="allocateRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="目标店铺" prop="to_shop_id">
|
||||
<ElSelect v-model="allocateForm.to_shop_id" placeholder="请选择目标店铺" style="width: 100%">
|
||||
<ElSelect
|
||||
v-model="allocateForm.to_shop_id"
|
||||
placeholder="请选择目标店铺"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="店铺A" :value="1" />
|
||||
<ElOption label="店铺B" :value="2" />
|
||||
<ElOption label="店铺C" :value="3" />
|
||||
@@ -136,22 +161,40 @@
|
||||
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="allocateForm.selection_type === 'range'" label="起始ICCID" prop="iccid_start">
|
||||
<ElFormItem
|
||||
v-if="allocateForm.selection_type === 'range'"
|
||||
label="起始ICCID"
|
||||
prop="iccid_start"
|
||||
>
|
||||
<ElInput v-model="allocateForm.iccid_start" placeholder="请输入起始ICCID" />
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="allocateForm.selection_type === 'range'" label="结束ICCID" prop="iccid_end">
|
||||
<ElFormItem
|
||||
v-if="allocateForm.selection_type === 'range'"
|
||||
label="结束ICCID"
|
||||
prop="iccid_end"
|
||||
>
|
||||
<ElInput v-model="allocateForm.iccid_end" placeholder="请输入结束ICCID" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="运营商">
|
||||
<ElSelect v-model="allocateForm.carrier_id" placeholder="请选择运营商" clearable style="width: 100%">
|
||||
<ElSelect
|
||||
v-model="allocateForm.carrier_id"
|
||||
placeholder="请选择运营商"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="中国移动" :value="1" />
|
||||
<ElOption label="中国联通" :value="2" />
|
||||
<ElOption label="中国电信" :value="3" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="卡状态">
|
||||
<ElSelect v-model="allocateForm.status" placeholder="请选择状态" clearable style="width: 100%">
|
||||
<ElSelect
|
||||
v-model="allocateForm.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="在库" :value="1" />
|
||||
<ElOption label="已分销" :value="2" />
|
||||
<ElOption label="已激活" :value="3" />
|
||||
@@ -163,7 +206,12 @@
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注">
|
||||
<ElInput v-model="allocateForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
|
||||
<ElInput
|
||||
v-model="allocateForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -185,7 +233,11 @@
|
||||
>
|
||||
<ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules" label-width="120px">
|
||||
<ElFormItem label="来源店铺" prop="from_shop_id">
|
||||
<ElSelect v-model="recallForm.from_shop_id" placeholder="请选择来源店铺" style="width: 100%">
|
||||
<ElSelect
|
||||
v-model="recallForm.from_shop_id"
|
||||
placeholder="请选择来源店铺"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="店铺A" :value="1" />
|
||||
<ElOption label="店铺B" :value="2" />
|
||||
<ElOption label="店铺C" :value="3" />
|
||||
@@ -203,15 +255,28 @@
|
||||
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="recallForm.selection_type === 'range'" label="起始ICCID" prop="iccid_start">
|
||||
<ElFormItem
|
||||
v-if="recallForm.selection_type === 'range'"
|
||||
label="起始ICCID"
|
||||
prop="iccid_start"
|
||||
>
|
||||
<ElInput v-model="recallForm.iccid_start" placeholder="请输入起始ICCID" />
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="recallForm.selection_type === 'range'" label="结束ICCID" prop="iccid_end">
|
||||
<ElFormItem
|
||||
v-if="recallForm.selection_type === 'range'"
|
||||
label="结束ICCID"
|
||||
prop="iccid_end"
|
||||
>
|
||||
<ElInput v-model="recallForm.iccid_end" placeholder="请输入结束ICCID" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="recallForm.selection_type === 'filter'" label="运营商">
|
||||
<ElSelect v-model="recallForm.carrier_id" placeholder="请选择运营商" clearable style="width: 100%">
|
||||
<ElSelect
|
||||
v-model="recallForm.carrier_id"
|
||||
placeholder="请选择运营商"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="中国移动" :value="1" />
|
||||
<ElOption label="中国联通" :value="2" />
|
||||
<ElOption label="中国电信" :value="3" />
|
||||
@@ -222,7 +287,12 @@
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注">
|
||||
<ElInput v-model="recallForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
|
||||
<ElInput
|
||||
v-model="recallForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -236,14 +306,14 @@
|
||||
</ElDialog>
|
||||
|
||||
<!-- 分配结果对话框 -->
|
||||
<ElDialog
|
||||
v-model="resultDialogVisible"
|
||||
:title="resultTitle"
|
||||
width="700px"
|
||||
>
|
||||
<ElDialog v-model="resultDialogVisible" :title="resultTitle" width="700px">
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="操作单号">{{ allocationResult.allocation_no }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="待处理总数">{{ allocationResult.total_count }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="操作单号">{{
|
||||
allocationResult.allocation_no
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="待处理总数">{{
|
||||
allocationResult.total_count
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<ElTag type="success">{{ allocationResult.success_count }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
@@ -252,7 +322,10 @@
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<div v-if="allocationResult.failed_items && allocationResult.failed_items.length > 0" style="margin-top: 20px">
|
||||
<div
|
||||
v-if="allocationResult.failed_items && allocationResult.failed_items.length > 0"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<ElDivider content-position="left">失败项详情</ElDivider>
|
||||
<ElTable :data="allocationResult.failed_items" border max-height="300">
|
||||
<ElTableColumn prop="iccid" label="ICCID" width="180" />
|
||||
@@ -274,7 +347,12 @@
|
||||
width="600px"
|
||||
@close="handleSeriesBindingDialogClose"
|
||||
>
|
||||
<ElForm ref="seriesBindingFormRef" :model="seriesBindingForm" :rules="seriesBindingRules" label-width="120px">
|
||||
<ElForm
|
||||
ref="seriesBindingFormRef"
|
||||
:model="seriesBindingForm"
|
||||
:rules="seriesBindingRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="已选择卡数">
|
||||
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
||||
</ElFormItem>
|
||||
@@ -307,11 +385,7 @@
|
||||
</ElDialog>
|
||||
|
||||
<!-- 套餐系列绑定结果对话框 -->
|
||||
<ElDialog
|
||||
v-model="seriesBindingResultDialogVisible"
|
||||
title="设置结果"
|
||||
width="700px"
|
||||
>
|
||||
<ElDialog v-model="seriesBindingResultDialogVisible" title="设置结果" width="700px">
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<ElTag type="success">{{ seriesBindingResult.success_count }}</ElTag>
|
||||
@@ -321,7 +395,10 @@
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<div v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0" style="margin-top: 20px">
|
||||
<div
|
||||
v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<ElDivider content-position="left">失败项详情</ElDivider>
|
||||
<ElTable :data="seriesBindingResult.failed_items" border max-height="300">
|
||||
<ElTableColumn prop="iccid" label="ICCID" width="180" />
|
||||
@@ -331,11 +408,99 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton type="primary" @click="seriesBindingResultDialogVisible = false">确定</ElButton>
|
||||
<ElButton type="primary" @click="seriesBindingResultDialogVisible = false"
|
||||
>确定</ElButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 卡详情对话框 -->
|
||||
<ElDialog v-model="cardDetailDialogVisible" title="卡片详情" width="900px">
|
||||
<div v-if="cardDetailLoading" style="text-align: center; padding: 40px">
|
||||
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||
<div style="margin-top: 16px">加载中...</div>
|
||||
</div>
|
||||
|
||||
<ElDescriptions v-else-if="currentCardDetail" :column="3" border>
|
||||
<ElDescriptionsItem label="卡ID">{{ currentCardDetail.id }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="ICCID" :span="2">{{
|
||||
currentCardDetail.iccid
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="卡接入号">{{
|
||||
currentCardDetail.msisdn || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="运营商">{{
|
||||
currentCardDetail.carrier_name || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="运营商类型">{{
|
||||
getCarrierTypeText(currentCardDetail.carrier_type)
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="卡类型">{{
|
||||
currentCardDetail.card_type || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="卡业务类型">{{
|
||||
getCardCategoryText(currentCardDetail.card_category)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成本价">{{
|
||||
formatCardPrice(currentCardDetail.cost_price)
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="分销价">{{
|
||||
formatCardPrice(currentCardDetail.distribute_price)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="getCardDetailStatusTagType(currentCardDetail.status)">
|
||||
{{ getCardDetailStatusText(currentCardDetail.status) }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="激活状态">
|
||||
<ElTag :type="currentCardDetail.activation_status === 1 ? 'success' : 'info'">
|
||||
{{ currentCardDetail.activation_status === 1 ? '已激活' : '未激活' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="实名状态">
|
||||
<ElTag :type="currentCardDetail.real_name_status === 1 ? 'success' : 'warning'">
|
||||
{{ currentCardDetail.real_name_status === 1 ? '已实名' : '未实名' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="网络状态">
|
||||
<ElTag :type="currentCardDetail.network_status === 1 ? 'success' : 'danger'">
|
||||
{{ currentCardDetail.network_status === 1 ? '开机' : '停机' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="累计流量使用"
|
||||
>{{ currentCardDetail.data_usage_mb }} MB</ElDescriptionsItem
|
||||
>
|
||||
|
||||
<ElDescriptionsItem label="首次佣金">
|
||||
<ElTag :type="currentCardDetail.first_commission_paid ? 'success' : 'info'">
|
||||
{{ currentCardDetail.first_commission_paid ? '已支付' : '未支付' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="累计充值">{{
|
||||
formatCardPrice(currentCardDetail.accumulated_recharge)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{
|
||||
formatDateTime(currentCardDetail.created_at)
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="更新时间" :span="2">{{
|
||||
formatDateTime(currentCardDetail.updated_at)
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElEmpty v-else description="未找到卡片信息" />
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton type="primary" @click="cardDetailDialogVisible = false">关闭</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -343,9 +508,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { CardService, StorageService } from '@/api/modules'
|
||||
import { ShopSeriesAllocationService } from '@/api/modules/shopSeriesAllocation'
|
||||
import { ElMessage, ElTag, ElUpload } from 'element-plus'
|
||||
import { ElMessage, ElTag, ElUpload, ElIcon } from 'element-plus'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
@@ -362,6 +529,7 @@
|
||||
|
||||
defineOptions({ name: 'StandaloneCardList' })
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const importLoading = ref(false)
|
||||
@@ -405,13 +573,17 @@
|
||||
failed_items: null
|
||||
})
|
||||
|
||||
// 卡详情弹窗相关
|
||||
const cardDetailDialogVisible = ref(false)
|
||||
const cardDetailLoading = ref(false)
|
||||
const currentCardDetail = ref<any>(null)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
status: undefined,
|
||||
carrier_id: undefined,
|
||||
iccid: '',
|
||||
msisdn: '',
|
||||
batch_no: '',
|
||||
is_distributed: undefined
|
||||
}
|
||||
|
||||
@@ -576,15 +748,6 @@
|
||||
placeholder: '请输入卡接入号'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '批次号',
|
||||
prop: 'batch_no',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入批次号'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '是否已分销',
|
||||
prop: 'is_distributed',
|
||||
@@ -653,12 +816,88 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 打开卡详情弹窗
|
||||
const goToCardDetail = async (iccid: string) => {
|
||||
cardDetailDialogVisible.value = true
|
||||
cardDetailLoading.value = true
|
||||
currentCardDetail.value = null
|
||||
|
||||
try {
|
||||
const res = await CardService.getIotCardDetailByIccid(iccid)
|
||||
if (res.code === 0 && res.data) {
|
||||
currentCardDetail.value = res.data
|
||||
} else {
|
||||
ElMessage.error(res.message || '查询失败')
|
||||
cardDetailDialogVisible.value = false
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询卡片详情失败:', error)
|
||||
ElMessage.error(error?.message || '查询失败,请检查ICCID是否正确')
|
||||
cardDetailDialogVisible.value = false
|
||||
} finally {
|
||||
cardDetailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 卡详情辅助函数
|
||||
const getCarrierTypeText = (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
CMCC: '中国移动',
|
||||
CUCC: '中国联通',
|
||||
CTCC: '中国电信',
|
||||
CBN: '中国广电'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
const getCardCategoryText = (category: string) => {
|
||||
const categoryMap: Record<string, string> = {
|
||||
normal: '普通卡',
|
||||
industry: '行业卡'
|
||||
}
|
||||
return categoryMap[category] || category
|
||||
}
|
||||
|
||||
const getCardDetailStatusText = (status: number) => {
|
||||
const statusMap: Record<number, string> = {
|
||||
1: '在库',
|
||||
2: '已分销',
|
||||
3: '已激活',
|
||||
4: '已停用'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
const getCardDetailStatusTagType = (status: number) => {
|
||||
const typeMap: Record<number, any> = {
|
||||
1: 'info',
|
||||
2: 'warning',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
const formatCardPrice = (price: number) => {
|
||||
return `¥${(price / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'iccid',
|
||||
label: 'ICCID',
|
||||
minWidth: 190
|
||||
minWidth: 200,
|
||||
formatter: (row: StandaloneIotCard) => {
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
style: { color: 'var(--el-color-primary)', cursor: 'pointer' },
|
||||
onClick: () => goToCardDetail(row.iccid)
|
||||
},
|
||||
row.iccid
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'msisdn',
|
||||
@@ -754,7 +993,7 @@
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 160,
|
||||
width: 180,
|
||||
formatter: (row: StandaloneIotCard) => formatDateTime(row.created_at)
|
||||
}
|
||||
])
|
||||
@@ -897,7 +1136,9 @@
|
||||
})
|
||||
|
||||
if (importRes.code === 0) {
|
||||
ElMessage.success(importRes.data.message || '导入任务已创建,请到任务管理页面查看导入进度')
|
||||
ElMessage.success(
|
||||
importRes.data.message || '导入任务已创建,请到任务管理页面查看导入进度'
|
||||
)
|
||||
importDialogVisible.value = false
|
||||
getTableData()
|
||||
}
|
||||
@@ -1167,7 +1408,9 @@
|
||||
} else if (res.data.success_count === 0) {
|
||||
ElMessage.error('套餐系列绑定设置失败')
|
||||
} else {
|
||||
ElMessage.warning(`部分设置成功:成功 ${res.data.success_count} 项,失败 ${res.data.fail_count} 项`)
|
||||
ElMessage.warning(
|
||||
`部分设置成功:成功 ${res.data.success_count} 项,失败 ${res.data.fail_count} 项`
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton type="primary" :loading="loading" @click="handleSearch">
|
||||
查询
|
||||
</ElButton>
|
||||
<ElButton type="primary" :loading="loading" @click="handleSearch"> 查询 </ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
@@ -44,9 +42,13 @@
|
||||
<ElDescriptionsItem label="卡接入号">{{ cardDetail.msisdn || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="运营商">{{ cardDetail.carrier_name }}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="运营商类型">{{ getCarrierTypeText(cardDetail.carrier_type) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="运营商类型">{{
|
||||
getCarrierTypeText(cardDetail.carrier_type)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="卡类型">{{ cardDetail.card_type }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="卡业务类型">{{ getCardCategoryText(cardDetail.card_category) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="卡业务类型">{{
|
||||
getCardCategoryText(cardDetail.card_category)
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="getStatusTagType(cardDetail.status)">
|
||||
@@ -73,11 +75,19 @@
|
||||
<ElDescriptionsItem label="供应商">{{ cardDetail.supplier || '--' }}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="店铺名称">{{ cardDetail.shop_name || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成本价">{{ formatPrice(cardDetail.cost_price) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="分销价">{{ formatPrice(cardDetail.distribute_price) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成本价">{{
|
||||
formatPrice(cardDetail.cost_price)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="分销价">{{
|
||||
formatPrice(cardDetail.distribute_price)
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="累计流量使用">{{ cardDetail.data_usage_mb }} MB</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="激活时间">{{ cardDetail.activated_at || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="累计流量使用"
|
||||
>{{ cardDetail.data_usage_mb }} MB</ElDescriptionsItem
|
||||
>
|
||||
<ElDescriptionsItem label="激活时间">{{
|
||||
cardDetail.activated_at || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{ cardDetail.created_at }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCard>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
@@ -15,7 +16,13 @@
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" :icon="Upload" @click="importDialogVisible = true">
|
||||
批量导入IoT卡
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
@@ -36,25 +43,152 @@
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 导入对话框 -->
|
||||
<ElDialog v-model="importDialogVisible" title="批量导入IoT卡" width="700px" 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. 请先下载 CSV 模板文件,按照模板格式填写IoT卡信息</p>
|
||||
<p>2. 支持 CSV 格式(.csv),单次最多导入 1000 条</p>
|
||||
<p>3. CSV 文件编码:UTF-8(推荐)或 GBK</p>
|
||||
<p>4. 必填字段:iccid(ICCID)、msisdn(MSISDN/手机号)</p>
|
||||
<p>5. 必须选择运营商</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<div style="margin-bottom: 20px">
|
||||
<ElButton type="primary" :icon="Download" @click="downloadTemplate">
|
||||
下载导入模板
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElFormItem label="运营商" required style="margin-bottom: 20px">
|
||||
<ElSelect v-model="selectedCarrierId" placeholder="请选择运营商" style="width: 100%">
|
||||
<ElOption label="中国移动" :value="1" />
|
||||
<ElOption label="中国联通" :value="2" />
|
||||
<ElOption label="中国电信" :value="3" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:limit="1"
|
||||
accept=".csv"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将 CSV 文件拖到此处,或<em>点击选择</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">只能上传 CSV 文件,且不超过 10MB</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="handleCancelImport">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="uploading"
|
||||
:disabled="!fileList.length || !selectedCarrierId"
|
||||
@click="submitUpload"
|
||||
>
|
||||
开始导入
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 任务详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="IoT卡导入任务详情" width="900px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="任务编号" :span="2">{{
|
||||
currentDetail.task_no
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="getStatusType(currentDetail.status)">{{ currentDetail.status_text }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="运营商">{{ currentDetail.carrier_name }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="文件名" :span="2">{{
|
||||
currentDetail.file_name
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="总数">{{ currentDetail.total_count }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.success_count }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="跳过数">{{ currentDetail.skip_count }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<span style="color: var(--el-color-danger)">{{ currentDetail.fail_count }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="警告数">
|
||||
<span style="color: var(--el-color-warning)">{{ currentDetail.warning_count }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="开始时间">{{ currentDetail.started_at }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="完成时间">{{ currentDetail.completed_at }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{ currentDetail.created_at }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="错误信息" :span="2">{{
|
||||
currentDetail.error_message
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDivider content-position="left">失败明细</ElDivider>
|
||||
<div
|
||||
v-if="currentDetail.failed_items && currentDetail.failed_items.length"
|
||||
style="max-height: 300px; overflow-y: auto"
|
||||
>
|
||||
<ElTable :data="currentDetail.failed_items" border size="small">
|
||||
<ElTableColumn label="行号" type="index" width="80" :index="(index) => index + 1" />
|
||||
<ElTableColumn label="ICCID" prop="iccid" width="200" />
|
||||
<ElTableColumn label="MSISDN" prop="msisdn" width="150" />
|
||||
<ElTableColumn label="失败原因" prop="reason" min-width="200">
|
||||
<template #default="{ row }">
|
||||
{{ row.reason || row.error || '未知错误' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
<ElEmpty v-else description="无失败记录" />
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
<ElButton
|
||||
v-if="currentDetail.fail_count > 0"
|
||||
type="primary"
|
||||
:icon="Download"
|
||||
@click="downloadFailData"
|
||||
>
|
||||
下载失败数据
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { CardService } from '@/api/modules'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { ElMessage, ElTag, ElFormItem, ElSelect, ElOption } from 'element-plus'
|
||||
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
|
||||
import type { UploadInstance } from 'element-plus'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { StorageService } from '@/api/modules/storage'
|
||||
import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card'
|
||||
|
||||
defineOptions({ name: 'IotCardTask' })
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const fileList = ref<File[]>([])
|
||||
const uploading = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const selectedCarrierId = ref<number>()
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
@@ -145,6 +279,7 @@
|
||||
]
|
||||
|
||||
const taskList = ref<IotCardImportTask[]>([])
|
||||
const currentDetail = ref<any>({})
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: IotCardImportTaskStatus) => {
|
||||
@@ -163,14 +298,24 @@
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (row: IotCardImportTask) => {
|
||||
router.push({
|
||||
path: '/asset-management/task-detail',
|
||||
query: {
|
||||
id: row.id,
|
||||
task_type: 'card'
|
||||
const viewDetail = async (row: IotCardImportTask) => {
|
||||
try {
|
||||
const res = await CardService.getIotCardImportTaskDetail(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
currentDetail.value = {
|
||||
...res.data,
|
||||
started_at: res.data.started_at ? formatDateTime(res.data.started_at) : '-',
|
||||
completed_at: res.data.completed_at ? formatDateTime(res.data.completed_at) : '-',
|
||||
created_at: res.data.created_at ? formatDateTime(res.data.created_at) : '-',
|
||||
carrier_name: res.data.carrier_name || '-',
|
||||
error_message: res.data.error_message || '-'
|
||||
}
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error)
|
||||
ElMessage.error('获取任务详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
@@ -196,7 +341,8 @@
|
||||
{
|
||||
prop: 'file_name',
|
||||
label: '文件名',
|
||||
minWidth: 250
|
||||
minWidth: 250,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'total_count',
|
||||
@@ -206,15 +352,17 @@
|
||||
{
|
||||
prop: 'success_count',
|
||||
label: '成功数',
|
||||
width: 80
|
||||
width: 80,
|
||||
formatter: (row: IotCardImportTask) => {
|
||||
return h('span', { style: { color: 'var(--el-color-success)' } }, row.success_count)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'fail_count',
|
||||
label: '失败数',
|
||||
width: 80,
|
||||
formatter: (row: IotCardImportTask) => {
|
||||
const type = row.fail_count > 0 ? 'danger' : 'success'
|
||||
return h(ElTag, { type, size: 'small' }, () => row.fail_count)
|
||||
return h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -225,37 +373,57 @@
|
||||
{
|
||||
prop: 'started_at',
|
||||
label: '开始时间',
|
||||
width: 160,
|
||||
width: 180,
|
||||
formatter: (row: IotCardImportTask) => (row.started_at ? formatDateTime(row.started_at) : '-')
|
||||
},
|
||||
{
|
||||
prop: 'completed_at',
|
||||
label: '完成时间',
|
||||
width: 160,
|
||||
formatter: (row: IotCardImportTask) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
|
||||
width: 180,
|
||||
formatter: (row: IotCardImportTask) =>
|
||||
row.completed_at ? formatDateTime(row.completed_at) : '-'
|
||||
},
|
||||
{
|
||||
prop: 'error_message',
|
||||
label: '错误信息',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: IotCardImportTask) => row.error_message || '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 160,
|
||||
width: 180,
|
||||
formatter: (row: IotCardImportTask) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 120,
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
formatter: (row: IotCardImportTask) => {
|
||||
return h(ArtButtonTable, {
|
||||
text: '查看详情',
|
||||
onClick: () => viewDetail(row)
|
||||
})
|
||||
const buttons = []
|
||||
|
||||
// 显示"查看详情"按钮
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '详情',
|
||||
onClick: () => viewDetail(row)
|
||||
})
|
||||
)
|
||||
|
||||
// 如果有失败数据,显示"失败数据"按钮
|
||||
if (row.fail_count > 0) {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '失败数据',
|
||||
type: 'danger',
|
||||
onClick: () => downloadFailDataByRow(row)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||
}
|
||||
}
|
||||
])
|
||||
@@ -330,10 +498,202 @@
|
||||
pagination.page = newCurrentPage
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 从行数据下载失败数据
|
||||
const downloadFailDataByRow = async (row: IotCardImportTask) => {
|
||||
try {
|
||||
const res = await CardService.getIotCardImportTaskDetail(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
const detail = res.data
|
||||
downloadFailDataFromDetail(detail, row.task_no)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败数据失败:', error)
|
||||
ElMessage.error('下载失败数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 下载失败数据(从详情对话框)
|
||||
const downloadFailData = () => {
|
||||
downloadFailDataFromDetail(currentDetail.value, currentDetail.value.task_no)
|
||||
}
|
||||
|
||||
// 下载失败数据的通用方法
|
||||
const downloadFailDataFromDetail = (detail: any, taskNo: string) => {
|
||||
const failReasons =
|
||||
detail.failed_items?.map((item: any, index: number) => ({
|
||||
row: index + 1,
|
||||
iccid: item.iccid || '-',
|
||||
msisdn: item.msisdn || '-',
|
||||
message: item.reason || item.error || '未知错误'
|
||||
})) || []
|
||||
|
||||
if (failReasons.length === 0) {
|
||||
ElMessage.warning('没有失败数据可下载')
|
||||
return
|
||||
}
|
||||
|
||||
const headers = ['行号', 'ICCID', 'MSISDN', '失败原因']
|
||||
const csvRows = [
|
||||
headers.join(','),
|
||||
...failReasons.map((item: any) =>
|
||||
[item.row, item.iccid, item.msisdn, `"${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', `IoT卡导入失败数据_${taskNo}.csv`)
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('失败数据下载成功')
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
const csvContent = [
|
||||
'iccid,msisdn',
|
||||
'89860123456789012345,13800138000',
|
||||
'89860123456789012346,13800138001',
|
||||
'89860123456789012347,13800138002'
|
||||
].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', 'IoT卡导入模板.csv')
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('IoT卡导入模板下载成功')
|
||||
}
|
||||
|
||||
// 文件选择变化
|
||||
const handleFileChange = (uploadFile: any) => {
|
||||
const maxSize = 10 * 1024 * 1024
|
||||
if (uploadFile.raw && uploadFile.raw.size > maxSize) {
|
||||
ElMessage.error('文件大小不能超过 10MB')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.csv')) {
|
||||
ElMessage.error('只能上传 CSV 文件')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
fileList.value = uploadFile.raw ? [uploadFile.raw] : []
|
||||
}
|
||||
|
||||
// 清空文件
|
||||
const clearFiles = () => {
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
selectedCarrierId.value = undefined
|
||||
}
|
||||
|
||||
// 取消导入
|
||||
const handleCancelImport = () => {
|
||||
clearFiles()
|
||||
importDialogVisible.value = false
|
||||
}
|
||||
|
||||
// 提交上传
|
||||
const submitUpload = async () => {
|
||||
if (!selectedCarrierId.value) {
|
||||
ElMessage.warning('请先选择运营商')
|
||||
return
|
||||
}
|
||||
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择CSV文件')
|
||||
return
|
||||
}
|
||||
|
||||
const file = fileList.value[0]
|
||||
uploading.value = true
|
||||
|
||||
try {
|
||||
ElMessage.info('正在准备上传...')
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: 'text/csv',
|
||||
purpose: 'iot_import'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
|
||||
ElMessage.info('正在上传文件...')
|
||||
await StorageService.uploadFile(upload_url, file, 'text/csv')
|
||||
|
||||
ElMessage.info('正在创建导入任务...')
|
||||
const importRes = await CardService.importIotCards({
|
||||
carrier_id: selectedCarrierId.value,
|
||||
file_key,
|
||||
batch_no: `IOT-${Date.now()}`
|
||||
})
|
||||
|
||||
if (importRes.code !== 0) {
|
||||
throw new Error(importRes.msg || '创建导入任务失败')
|
||||
}
|
||||
|
||||
const taskNo = importRes.data.task_no
|
||||
|
||||
handleCancelImport()
|
||||
getTableData()
|
||||
|
||||
ElMessage.success({
|
||||
message: `导入任务已创建!任务编号:${taskNo}`,
|
||||
duration: 3000,
|
||||
showClose: true
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('IoT卡导入失败:', error)
|
||||
ElMessage.error(error.message || 'IoT卡导入失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.iot-card-task-page {
|
||||
// IoT card task page styles
|
||||
:deep(.el-icon--upload) {
|
||||
margin-bottom: 16px;
|
||||
font-size: 67px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
:deep(.el-upload__text) {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -62,18 +62,8 @@
|
||||
</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="device_no"
|
||||
label="设备号"
|
||||
min-width="180"
|
||||
/>
|
||||
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
|
||||
<ElTableColumn v-else prop="device_no" label="设备号" min-width="180" />
|
||||
<ElTableColumn prop="reason" label="失败原因" min-width="300" />
|
||||
</ElTable>
|
||||
</div>
|
||||
@@ -85,18 +75,8 @@
|
||||
</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="device_no"
|
||||
label="设备号"
|
||||
min-width="180"
|
||||
/>
|
||||
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
|
||||
<ElTableColumn v-else prop="device_no" label="设备号" min-width="180" />
|
||||
<ElTableColumn prop="reason" label="跳过原因" min-width="300" />
|
||||
</ElTable>
|
||||
</div>
|
||||
@@ -108,7 +88,15 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { CardService, DeviceService } from '@/api/modules'
|
||||
import { ElMessage, ElTag, ElDescriptions, ElDescriptionsItem, ElDivider, ElTable, ElTableColumn } from 'element-plus'
|
||||
import {
|
||||
ElMessage,
|
||||
ElTag,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElDivider,
|
||||
ElTable,
|
||||
ElTableColumn
|
||||
} from 'element-plus'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type { IotCardImportTaskDetail, IotCardImportTaskStatus } from '@/types/api/card'
|
||||
import type { DeviceImportTaskDetail } from '@/types/api/device'
|
||||
|
||||
@@ -15,8 +15,14 @@
|
||||
<p>1. 请先下载 CSV 模板文件,按照模板格式填写设备信息</p>
|
||||
<p>2. 支持 CSV 格式(.csv),单次最多导入 1000 条</p>
|
||||
<p>3. CSV 文件编码:UTF-8(推荐)或 GBK</p>
|
||||
<p>4. 必填字段:device_no(设备号)、device_name(设备名称)、device_model(设备型号)</p>
|
||||
<p>5. 可选字段:device_type(设备类型)、manufacturer(制造商)、max_sim_slots(最大插槽数,默认1)</p>
|
||||
<p
|
||||
>4.
|
||||
必填字段:device_no(设备号)、device_name(设备名称)、device_model(设备型号)</p
|
||||
>
|
||||
<p
|
||||
>5.
|
||||
可选字段:device_type(设备类型)、manufacturer(制造商)、max_sim_slots(最大插槽数,默认1)</p
|
||||
>
|
||||
<p>6. 设备号重复将自动跳过,导入后可在任务管理中查看详情</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -59,52 +65,6 @@
|
||||
</ElRow>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入统计 -->
|
||||
<ElRow :gutter="20" style="margin-top: 20px">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">今日导入</div>
|
||||
<div class="stat-value">1,250</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-primary)"><Upload /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">成功绑定</div>
|
||||
<div class="stat-value" style="color: var(--el-color-success)">1,180</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-success)"
|
||||
><SuccessFilled
|
||||
/></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">导入失败</div>
|
||||
<div class="stat-value" style="color: var(--el-color-danger)">70</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-danger)"
|
||||
><CircleCloseFilled
|
||||
/></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">成功率</div>
|
||||
<div class="stat-value">94.4%</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-warning)"
|
||||
><TrendCharts
|
||||
/></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 导入记录 -->
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
@@ -117,71 +77,27 @@
|
||||
style="width: 120px; margin-right: 12px"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="处理中" value="processing" />
|
||||
<ElOption label="完成" value="success" />
|
||||
<ElOption label="失败" value="failed" />
|
||||
<ElOption label="全部" :value="null" />
|
||||
<ElOption label="待处理" :value="1" />
|
||||
<ElOption label="处理中" :value="2" />
|
||||
<ElOption label="已完成" :value="3" />
|
||||
<ElOption label="失败" :value="4" />
|
||||
</ElSelect>
|
||||
<ElButton @click="refreshList">刷新</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ArtTable :data="filteredRecords" index>
|
||||
<ArtTable
|
||||
rowKey="id"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:data="filteredRecords"
|
||||
:marginTop="10"
|
||||
:stripe="false"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="导入批次号" prop="batchNo" width="180" />
|
||||
<ElTableColumn label="文件名" prop="fileName" min-width="200" show-overflow-tooltip />
|
||||
<ElTableColumn label="设备总数" prop="totalCount" width="100" />
|
||||
<ElTableColumn label="成功数" prop="successCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">{{ scope.row.successCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="失败数" prop="failCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-danger)">{{ scope.row.failCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="已绑定ICCID" prop="bindCount" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag type="success" size="small">{{ scope.row.bindCount }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入状态" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'processing'" type="warning">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
处理中
|
||||
</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'success'" type="success">完成</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'failed'" type="danger">失败</ElTag>
|
||||
<ElTag v-else type="info">待处理</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入进度" prop="progress" width="150">
|
||||
<template #default="scope">
|
||||
<ElProgress
|
||||
:percentage="scope.row.progress"
|
||||
:status="scope.row.status === 'failed' ? 'exception' : undefined"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入时间" prop="importTime" width="180" />
|
||||
<ElTableColumn label="操作人" prop="operator" width="120" />
|
||||
<ElTableColumn fixed="right" label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewDetail(scope.row)">查看详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.failCount > 0"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Download"
|
||||
@click="downloadFailData(scope.row)"
|
||||
>
|
||||
失败数据
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
@@ -189,20 +105,31 @@
|
||||
<!-- 导入详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="设备导入详情" width="900px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="任务编号" :span="2">{{
|
||||
currentDetail.taskNo
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">{{ currentDetail.statusText }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="批次号">{{ currentDetail.batchNo }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="文件名">{{ currentDetail.fileName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备总数">{{ currentDetail.totalCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功导入">
|
||||
<ElDescriptionsItem label="文件名" :span="2">{{
|
||||
currentDetail.fileName
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="总数">{{ currentDetail.totalCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.successCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="导入失败">
|
||||
<ElDescriptionsItem label="跳过数">{{ currentDetail.skipCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已绑定ICCID">
|
||||
<ElTag type="success">{{ currentDetail.bindCount }}</ElTag>
|
||||
<ElDescriptionsItem label="警告数">
|
||||
<span style="color: var(--el-color-warning)">{{ currentDetail.warningCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="导入时间">{{ currentDetail.importTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="操作人">{{ currentDetail.operator }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="开始时间">{{ currentDetail.startedAt }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="完成时间">{{ currentDetail.completedAt }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{ currentDetail.createdAt }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="错误信息" :span="2">{{
|
||||
currentDetail.errorMessage
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDivider content-position="left">失败明细</ElDivider>
|
||||
@@ -235,22 +162,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { h, computed, watch } from 'vue'
|
||||
import { ElMessage, ElTag, ElProgress, ElIcon, ElButton } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
Download,
|
||||
UploadFilled,
|
||||
View,
|
||||
Loading,
|
||||
Upload,
|
||||
SuccessFilled,
|
||||
CircleCloseFilled,
|
||||
TrendCharts
|
||||
} from '@element-plus/icons-vue'
|
||||
import { Download, UploadFilled, View, Loading } from '@element-plus/icons-vue'
|
||||
import type { UploadInstance } from 'element-plus'
|
||||
import { StorageService } from '@/api/modules/storage'
|
||||
import { DeviceService } from '@/api/modules'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
|
||||
defineOptions({ name: 'DeviceImport' })
|
||||
|
||||
@@ -265,107 +186,213 @@
|
||||
|
||||
interface ImportRecord {
|
||||
id: string
|
||||
taskNo: string
|
||||
status: number
|
||||
statusText: string
|
||||
batchNo: string
|
||||
fileName: string
|
||||
totalCount: number
|
||||
successCount: number
|
||||
skipCount: number
|
||||
failCount: number
|
||||
bindCount: number
|
||||
status: 'pending' | 'processing' | 'success' | 'failed'
|
||||
progress: number
|
||||
importTime: string
|
||||
operator: string
|
||||
warningCount: number
|
||||
startedAt: string
|
||||
completedAt: string
|
||||
errorMessage: string
|
||||
createdAt: string
|
||||
failReasons?: FailReason[]
|
||||
}
|
||||
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const tableRef = ref()
|
||||
const fileList = ref<File[]>([])
|
||||
const uploading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const statusFilter = ref('')
|
||||
const statusFilter = ref<number | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const importRecords = ref<ImportRecord[]>([
|
||||
{
|
||||
id: '1',
|
||||
batchNo: 'DEV20260109001',
|
||||
fileName: '设备导入模板_20260109.xlsx',
|
||||
totalCount: 300,
|
||||
successCount: 285,
|
||||
failCount: 15,
|
||||
bindCount: 285,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-09 11:30:00',
|
||||
operator: 'admin',
|
||||
failReasons: [
|
||||
{ row: 12, deviceCode: 'DEV001', iccid: '89860123456789012345', message: 'ICCID 不存在' },
|
||||
{ row: 23, deviceCode: 'DEV002', iccid: '89860123456789012346', message: '设备编号已存在' },
|
||||
{ row: 45, deviceCode: '', iccid: '89860123456789012347', message: '设备编号为空' },
|
||||
{ row: 67, deviceCode: 'DEV003', iccid: '', message: 'ICCID 为空' },
|
||||
{ row: 89, deviceCode: 'DEV004', iccid: '89860123456789012348', message: '设备类型不存在' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
batchNo: 'DEV20260108001',
|
||||
fileName: '智能水表设备批量导入.xlsx',
|
||||
totalCount: 150,
|
||||
successCount: 150,
|
||||
failCount: 0,
|
||||
bindCount: 150,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-08 14:20:00',
|
||||
operator: 'admin'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
batchNo: 'DEV20260107001',
|
||||
fileName: 'GPS定位器导入.xlsx',
|
||||
totalCount: 200,
|
||||
successCount: 180,
|
||||
failCount: 20,
|
||||
bindCount: 180,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-07 10:15:00',
|
||||
operator: 'operator01',
|
||||
failReasons: [
|
||||
{
|
||||
row: 10,
|
||||
deviceCode: 'GPS001',
|
||||
iccid: '89860123456789012349',
|
||||
message: 'ICCID 已被其他设备绑定'
|
||||
},
|
||||
{ row: 20, deviceCode: 'GPS002', iccid: '89860123456789012350', message: 'ICCID 状态异常' }
|
||||
]
|
||||
}
|
||||
])
|
||||
const importRecords = ref<ImportRecord[]>([])
|
||||
|
||||
const currentDetail = ref<ImportRecord>({
|
||||
id: '',
|
||||
taskNo: '',
|
||||
status: 1,
|
||||
statusText: '',
|
||||
batchNo: '',
|
||||
fileName: '',
|
||||
totalCount: 0,
|
||||
successCount: 0,
|
||||
skipCount: 0,
|
||||
failCount: 0,
|
||||
bindCount: 0,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
importTime: '',
|
||||
operator: ''
|
||||
warningCount: 0,
|
||||
startedAt: '',
|
||||
completedAt: '',
|
||||
errorMessage: '',
|
||||
createdAt: ''
|
||||
})
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
if (!statusFilter.value) return importRecords.value
|
||||
if (statusFilter.value === null) return importRecords.value
|
||||
return importRecords.value.filter((item) => item.status === statusFilter.value)
|
||||
})
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'taskNo',
|
||||
label: '任务编号',
|
||||
width: 200,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 120,
|
||||
formatter: (row: ImportRecord) => {
|
||||
if (row.status === 1) {
|
||||
return h(ElTag, { type: 'info' }, () => '待处理')
|
||||
} else if (row.status === 2) {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: 'warning' },
|
||||
{
|
||||
default: () => [
|
||||
h(ElIcon, { class: 'is-loading' }, () => h(Loading)),
|
||||
h('span', { style: { marginLeft: '4px' } }, '处理中')
|
||||
]
|
||||
}
|
||||
)
|
||||
} else if (row.status === 3) {
|
||||
return h(ElTag, { type: 'success' }, () => '已完成')
|
||||
} else if (row.status === 4) {
|
||||
return h(ElTag, { type: 'danger' }, () => '失败')
|
||||
} else {
|
||||
return h(ElTag, { type: 'info' }, () => row.statusText || '-')
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'batchNo',
|
||||
label: '批次号',
|
||||
width: 180,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'fileName',
|
||||
label: '文件名',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'totalCount',
|
||||
label: '总数',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'successCount',
|
||||
label: '成功数',
|
||||
width: 100,
|
||||
formatter: (row: ImportRecord) => {
|
||||
return h('span', { style: { color: 'var(--el-color-success)' } }, row.successCount)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'skipCount',
|
||||
label: '跳过数',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'failCount',
|
||||
label: '失败数',
|
||||
width: 100,
|
||||
formatter: (row: ImportRecord) => {
|
||||
return h('span', { style: { color: 'var(--el-color-danger)' } }, row.failCount)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'warningCount',
|
||||
label: '警告数',
|
||||
width: 100,
|
||||
formatter: (row: ImportRecord) => {
|
||||
return h('span', { style: { color: 'var(--el-color-warning)' } }, row.warningCount)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'startedAt',
|
||||
label: '开始时间',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
prop: 'completedAt',
|
||||
label: '完成时间',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
prop: 'errorMessage',
|
||||
label: '错误信息',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'createdAt',
|
||||
label: '创建时间',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
formatter: (row: ImportRecord) => {
|
||||
const buttons = []
|
||||
|
||||
// 显示"查看详情"按钮
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '详情',
|
||||
onClick: () => viewDetail(row)
|
||||
})
|
||||
),
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '失败数据',
|
||||
type: 'danger',
|
||||
onClick: () => downloadFailData(row)
|
||||
})
|
||||
)
|
||||
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('模板下载中...')
|
||||
setTimeout(() => {
|
||||
ElMessage.success('设备导入模板下载成功')
|
||||
}, 1000)
|
||||
// CSV模板内容 - 包含表头和示例数据
|
||||
const csvContent = [
|
||||
// 表头
|
||||
'device_no,device_name,device_model,device_type,manufacturer,max_sim_slots',
|
||||
// 示例数据
|
||||
'DEV001,智能水表01,WM-2000,智能水表,华为,1',
|
||||
'DEV002,GPS定位器01,GPS-3000,定位设备,小米,2',
|
||||
'DEV003,智能燃气表01,GM-1500,智能燃气表,海尔,1'
|
||||
].join('\n')
|
||||
|
||||
// 添加 BOM 头确保 Excel 正确识别 UTF-8 编码
|
||||
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', '设备导入模板.csv')
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('设备导入模板下载成功')
|
||||
}
|
||||
|
||||
const handleFileChange = (uploadFile: any) => {
|
||||
@@ -444,17 +471,15 @@
|
||||
// 清空文件列表
|
||||
clearFiles()
|
||||
|
||||
// 显示成功消息并提供跳转链接
|
||||
// 刷新任务列表
|
||||
await fetchImportTasks()
|
||||
|
||||
// 显示成功消息
|
||||
ElMessage.success({
|
||||
message: `导入任务已创建!任务编号:${taskNo}`,
|
||||
duration: 5000,
|
||||
duration: 3000,
|
||||
showClose: true
|
||||
})
|
||||
|
||||
// 3秒后跳转到任务管理页面
|
||||
setTimeout(() => {
|
||||
router.push('/asset-management/task-management')
|
||||
}, 3000)
|
||||
} catch (error: any) {
|
||||
console.error('设备导入失败:', error)
|
||||
ElMessage.error(error.message || '设备导入失败')
|
||||
@@ -463,21 +488,133 @@
|
||||
}
|
||||
}
|
||||
|
||||
const refreshList = () => {
|
||||
ElMessage.success('刷新成功')
|
||||
// 获取导入任务列表
|
||||
const fetchImportTasks = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 100
|
||||
}
|
||||
|
||||
// 如果有状态筛选,添加到参数
|
||||
if (statusFilter.value !== null) {
|
||||
params.status = statusFilter.value
|
||||
}
|
||||
|
||||
const res = await DeviceService.getImportTasks(params)
|
||||
if (res.code === 0 && res.data) {
|
||||
// 将API返回的数据映射到本地格式
|
||||
importRecords.value = res.data.items.map((item: any) => ({
|
||||
id: item.id.toString(),
|
||||
taskNo: item.task_no || '-',
|
||||
status: item.status,
|
||||
statusText: item.status_text || '-',
|
||||
batchNo: item.batch_no || '-',
|
||||
fileName: item.file_name || '-',
|
||||
totalCount: item.total_count || 0,
|
||||
successCount: item.success_count || 0,
|
||||
skipCount: item.skip_count || 0,
|
||||
failCount: item.fail_count || 0,
|
||||
warningCount: item.warning_count || 0,
|
||||
startedAt: item.started_at ? formatDateTime(item.started_at) : '-',
|
||||
completedAt: item.completed_at ? formatDateTime(item.completed_at) : '-',
|
||||
errorMessage: item.error_message || '-',
|
||||
createdAt: item.created_at ? formatDateTime(item.created_at) : '-'
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取导入任务列表失败:', error)
|
||||
ElMessage.error('获取导入任务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const viewDetail = (row: ImportRecord) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
const refreshList = () => {
|
||||
fetchImportTasks()
|
||||
}
|
||||
|
||||
const viewDetail = async (row: ImportRecord) => {
|
||||
try {
|
||||
const res = await DeviceService.getImportTaskDetail(Number(row.id))
|
||||
if (res.code === 0 && res.data) {
|
||||
const detail = res.data
|
||||
currentDetail.value = {
|
||||
id: detail.id.toString(),
|
||||
taskNo: detail.task_no || '-',
|
||||
status: detail.status,
|
||||
statusText: detail.status_text || '-',
|
||||
batchNo: detail.batch_no || '-',
|
||||
fileName: detail.file_name || '-',
|
||||
totalCount: detail.total_count || 0,
|
||||
successCount: detail.success_count || 0,
|
||||
skipCount: detail.skip_count || 0,
|
||||
failCount: detail.fail_count || 0,
|
||||
warningCount: detail.warning_count || 0,
|
||||
startedAt: detail.started_at ? formatDateTime(detail.started_at) : '-',
|
||||
completedAt: detail.completed_at ? formatDateTime(detail.completed_at) : '-',
|
||||
errorMessage: detail.error_message || '-',
|
||||
createdAt: detail.created_at ? formatDateTime(detail.created_at) : '-',
|
||||
failReasons:
|
||||
detail.failed_items?.map((item: any, index: number) => ({
|
||||
row: index + 1,
|
||||
deviceCode: item.device_no || '-',
|
||||
iccid: item.iccid || '-',
|
||||
message: item.reason || item.error || '未知错误'
|
||||
})) || []
|
||||
}
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error)
|
||||
ElMessage.error('获取任务详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const downloadFailData = (row: ImportRecord) => {
|
||||
ElMessage.info(`正在下载批次 ${row.batchNo} 的失败数据...`)
|
||||
setTimeout(() => {
|
||||
ElMessage.success('失败数据下载完成')
|
||||
}, 1000)
|
||||
if (!row.failReasons || row.failReasons.length === 0) {
|
||||
ElMessage.warning('没有失败数据可下载')
|
||||
return
|
||||
}
|
||||
|
||||
// 生成失败数据CSV
|
||||
const headers = ['行号', '设备编号', 'ICCID', '失败原因']
|
||||
const csvRows = [
|
||||
headers.join(','),
|
||||
...row.failReasons.map((item) =>
|
||||
[item.row, item.deviceCode, item.iccid, `"${item.message}"`].join(',')
|
||||
)
|
||||
]
|
||||
const csvContent = csvRows.join('\n')
|
||||
|
||||
// 添加 BOM 头确保 Excel 正确识别 UTF-8 编码
|
||||
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.batchNo}.csv`)
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('失败数据下载成功')
|
||||
}
|
||||
|
||||
// 页面加载时获取任务列表
|
||||
onMounted(() => {
|
||||
fetchImportTasks()
|
||||
})
|
||||
|
||||
// 监听状态筛选变化
|
||||
watch(statusFilter, () => {
|
||||
fetchImportTasks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -125,7 +125,12 @@
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="订单号" prop="order_no" min-width="180" show-overflow-tooltip />
|
||||
<ElTableColumn label="ICCID" prop="iccid" min-width="150" show-overflow-tooltip />
|
||||
<ElTableColumn label="设备号" prop="device_no" min-width="150" show-overflow-tooltip />
|
||||
<ElTableColumn
|
||||
label="设备号"
|
||||
prop="device_no"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn label="入账时间" prop="created_at" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDateTime(scope.row.created_at) }}
|
||||
|
||||
@@ -54,10 +54,7 @@
|
||||
:placeholder="t('orderManagement.searchForm.orderTypePlaceholder')"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
:label="t('orderManagement.orderType.singleCard')"
|
||||
value="single_card"
|
||||
/>
|
||||
<ElOption :label="t('orderManagement.orderType.singleCard')" value="single_card" />
|
||||
<ElOption :label="t('orderManagement.orderType.device')" value="device" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
@@ -154,7 +151,10 @@
|
||||
</ElDescriptions>
|
||||
|
||||
<!-- 订单项列表 -->
|
||||
<div v-if="currentOrder.items && currentOrder.items.length > 0" style="margin-top: 20px">
|
||||
<div
|
||||
v-if="currentOrder.items && currentOrder.items.length > 0"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<h4>{{ t('orderManagement.orderItems') }}</h4>
|
||||
<ElTable :data="currentOrder.items" border style="margin-top: 10px">
|
||||
<ElTableColumn
|
||||
@@ -176,11 +176,7 @@
|
||||
{{ formatCurrency(row.unit_price) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="amount"
|
||||
:label="t('orderManagement.items.amount')"
|
||||
width="120"
|
||||
>
|
||||
<ElTableColumn prop="amount" :label="t('orderManagement.items.amount')" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatCurrency(row.amount) }}
|
||||
</template>
|
||||
@@ -415,10 +411,8 @@
|
||||
label: t('orderManagement.table.orderType'),
|
||||
width: 120,
|
||||
formatter: (row: Order) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: row.order_type === 'single_card' ? 'primary' : 'success' },
|
||||
() => getOrderTypeText(row.order_type)
|
||||
return h(ElTag, { type: row.order_type === 'single_card' ? 'primary' : 'success' }, () =>
|
||||
getOrderTypeText(row.order_type)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -427,10 +421,8 @@
|
||||
label: t('orderManagement.table.buyerType'),
|
||||
width: 120,
|
||||
formatter: (row: Order) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: row.buyer_type === 'personal' ? 'info' : 'warning' },
|
||||
() => getBuyerTypeText(row.buyer_type)
|
||||
return h(ElTag, { type: row.buyer_type === 'personal' ? 'info' : 'warning' }, () =>
|
||||
getBuyerTypeText(row.buyer_type)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -439,8 +431,10 @@
|
||||
label: t('orderManagement.table.paymentStatus'),
|
||||
width: 120,
|
||||
formatter: (row: Order) => {
|
||||
return h(ElTag, { type: getPaymentStatusType(row.payment_status) }, () =>
|
||||
row.payment_status_text
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: getPaymentStatusType(row.payment_status) },
|
||||
() => row.payment_status_text
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -48,7 +48,12 @@
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleCostPriceDialogClosed"
|
||||
>
|
||||
<ElForm ref="costPriceFormRef" :model="costPriceForm" :rules="costPriceRules" label-width="120px">
|
||||
<ElForm
|
||||
ref="costPriceFormRef"
|
||||
:model="costPriceForm"
|
||||
:rules="costPriceRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="套餐名称">
|
||||
<ElInput v-model="costPriceForm.package_name" disabled />
|
||||
</ElFormItem>
|
||||
@@ -56,7 +61,12 @@
|
||||
<ElInput v-model="costPriceForm.shop_name" disabled />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="原成本价(分)">
|
||||
<ElInputNumber v-model="costPriceForm.old_cost_price" disabled :controls="false" style="width: 100%" />
|
||||
<ElInputNumber
|
||||
v-model="costPriceForm.old_cost_price"
|
||||
disabled
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="新成本价(分)" prop="cost_price">
|
||||
<ElInputNumber
|
||||
@@ -71,7 +81,11 @@
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="costPriceDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleCostPriceSubmit(costPriceFormRef)" :loading="costPriceSubmitLoading">
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleCostPriceSubmit(costPriceFormRef)"
|
||||
:loading="costPriceSubmitLoading"
|
||||
>
|
||||
提交
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -154,11 +168,7 @@
|
||||
import { ShopPackageAllocationService, PackageManageService, ShopService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElSwitch } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
ShopPackageAllocationResponse,
|
||||
PackageResponse,
|
||||
ShopResponse
|
||||
} from '@/types/api'
|
||||
import type { ShopPackageAllocationResponse, PackageResponse, ShopResponse } from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
@@ -281,7 +291,9 @@
|
||||
if (value === undefined || value === null || value === '') {
|
||||
callback(new Error('请输入成本价'))
|
||||
} else if (form.package_base_price && value < form.package_base_price) {
|
||||
callback(new Error(`成本价不能低于套餐价格 ¥${(form.package_base_price / 100).toFixed(2)}`))
|
||||
callback(
|
||||
new Error(`成本价不能低于套餐价格 ¥${(form.package_base_price / 100).toFixed(2)}`)
|
||||
)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@@ -355,7 +367,8 @@
|
||||
prop: 'calculated_cost_price',
|
||||
label: '原计算成本价',
|
||||
width: 120,
|
||||
formatter: (row: ShopPackageAllocationResponse) => `¥${(row.calculated_cost_price / 100).toFixed(2)}`
|
||||
formatter: (row: ShopPackageAllocationResponse) =>
|
||||
`¥${(row.calculated_cost_price / 100).toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
@@ -624,7 +637,7 @@
|
||||
const handlePackageChange = (packageId: number | undefined) => {
|
||||
if (packageId) {
|
||||
// 从套餐选项中找到选中的套餐
|
||||
const selectedPackage = packageOptions.value.find(pkg => pkg.id === packageId)
|
||||
const selectedPackage = packageOptions.value.find((pkg) => pkg.id === packageId)
|
||||
if (selectedPackage) {
|
||||
// 将套餐的价格设置为成本价
|
||||
form.cost_price = selectedPackage.price
|
||||
|
||||
@@ -94,11 +94,7 @@
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="流量类型" prop="data_type">
|
||||
<ElSelect
|
||||
v-model="form.data_type"
|
||||
placeholder="请选择流量类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElSelect v-model="form.data_type" placeholder="请选择流量类型" style="width: 100%">
|
||||
<ElOption
|
||||
v-for="option in DATA_TYPE_OPTIONS"
|
||||
:key="option.value"
|
||||
@@ -116,7 +112,11 @@
|
||||
placeholder="请输入真流量额度"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="虚流量额度(MB)" prop="virtual_data_mb" v-if="form.data_type === 'virtual'">
|
||||
<ElFormItem
|
||||
label="虚流量额度(MB)"
|
||||
prop="virtual_data_mb"
|
||||
v-if="form.data_type === 'virtual'"
|
||||
>
|
||||
<ElInputNumber
|
||||
v-model="form.virtual_data_mb"
|
||||
:min="0"
|
||||
@@ -135,12 +135,7 @@
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="价格(分)" prop="price">
|
||||
<ElInputNumber
|
||||
v-model="form.price"
|
||||
:min="0"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<ElInputNumber v-model="form.price" :min="0" :controls="false" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="套餐描述" prop="description">
|
||||
<ElInput
|
||||
@@ -387,10 +382,8 @@
|
||||
label: '套餐类型',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: getPackageTypeTag(row.package_type), size: 'small' },
|
||||
() => getPackageTypeLabel(row.package_type)
|
||||
return h(ElTag, { type: getPackageTypeTag(row.package_type), size: 'small' }, () =>
|
||||
getPackageTypeLabel(row.package_type)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -399,10 +392,8 @@
|
||||
label: '流量类型',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: getDataTypeTag(row.data_type), size: 'small' },
|
||||
() => getDataTypeLabel(row.data_type)
|
||||
return h(ElTag, { type: getDataTypeTag(row.data_type), size: 'small' }, () =>
|
||||
getDataTypeLabel(row.data_type)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -176,7 +176,11 @@
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="form.one_time_commission_config.mode === 'fixed' ? '佣金金额(分)' : '佣金比例(千分比)'"
|
||||
:label="
|
||||
form.one_time_commission_config.mode === 'fixed'
|
||||
? '佣金金额(分)'
|
||||
: '佣金比例(千分比)'
|
||||
"
|
||||
prop="one_time_commission_config.value"
|
||||
>
|
||||
<ElInputNumber
|
||||
@@ -197,8 +201,16 @@
|
||||
<template v-if="form.one_time_commission_config.type === 'tiered'">
|
||||
<ElFormItem label="梯度档位">
|
||||
<div class="tier-list">
|
||||
<div v-for="(tier, index) in form.one_time_commission_config.tiers" :key="index" class="tier-item">
|
||||
<ElSelect v-model="tier.tier_type" placeholder="梯度类型" style="width: 120px">
|
||||
<div
|
||||
v-for="(tier, index) in form.one_time_commission_config.tiers"
|
||||
:key="index"
|
||||
class="tier-item"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="tier.tier_type"
|
||||
placeholder="梯度类型"
|
||||
style="width: 120px"
|
||||
>
|
||||
<ElOption label="销量" value="sales_count" />
|
||||
<ElOption label="销售额" value="sales_amount" />
|
||||
</ElSelect>
|
||||
@@ -903,12 +915,14 @@
|
||||
}
|
||||
// 梯度类型配置
|
||||
else if (form.one_time_commission_config.type === 'tiered') {
|
||||
data.one_time_commission_config.tiers = form.one_time_commission_config.tiers.map((t: any) => ({
|
||||
tier_type: t.tier_type,
|
||||
threshold: t.threshold,
|
||||
mode: t.mode,
|
||||
value: t.value
|
||||
}))
|
||||
data.one_time_commission_config.tiers = form.one_time_commission_config.tiers.map(
|
||||
(t: any) => ({
|
||||
tier_type: t.tier_type,
|
||||
threshold: t.threshold,
|
||||
mode: t.mode,
|
||||
value: t.value
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -962,9 +976,9 @@
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tier-list {
|
||||
@@ -981,26 +995,26 @@
|
||||
.info-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 18px;
|
||||
padding: 12px;
|
||||
margin-bottom: 18px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: var(--art-primary);
|
||||
font-weight: 500;
|
||||
color: var(--art-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -455,7 +455,8 @@
|
||||
activeText: getStatusText(CommonStatus.ENABLED),
|
||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||
inlinePrompt: true,
|
||||
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
|
||||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||
handleStatusChange(row, val as number)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -48,7 +48,12 @@
|
||||
<ElInput v-model="form.perm_name" placeholder="请输入权限名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="权限标识" prop="perm_code">
|
||||
<ElInput v-model="form.perm_code" placeholder="请输入权限标识,如:user:add" />
|
||||
<ElInput
|
||||
v-model="form.perm_code"
|
||||
placeholder="例如:菜单权限:menu:role 按钮权限: role:add"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="权限类型" prop="perm_type">
|
||||
<ElSelect v-model="form.perm_type" placeholder="请选择权限类型" style="width: 100%">
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton @click="showDialog('add')">新增角色</ElButton>
|
||||
<ElButton @click="showDialog('add')" v-permission="'role:add'">新增角色</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
@@ -59,7 +59,12 @@
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="角色类型" prop="role_type">
|
||||
<ElSelect v-model="form.role_type" placeholder="请选择角色类型" style="width: 100%">
|
||||
<ElSelect
|
||||
v-model="form.role_type"
|
||||
placeholder="请选择角色类型"
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
>
|
||||
<ElOption label="平台角色" :value="1" />
|
||||
<ElOption label="客户角色" :value="2" />
|
||||
</ElSelect>
|
||||
@@ -226,13 +231,12 @@
|
||||
},
|
||||
{
|
||||
prop: 'role_desc',
|
||||
label: '角色描述',
|
||||
minWidth: 150
|
||||
label: '角色描述'
|
||||
},
|
||||
{
|
||||
prop: 'role_type',
|
||||
label: '角色类型',
|
||||
width: 100,
|
||||
minWidth: 120,
|
||||
formatter: (row: any) => {
|
||||
return h(ElTag, { type: row.role_type === 1 ? 'primary' : 'success' }, () =>
|
||||
row.role_type === 1 ? '平台角色' : '客户角色'
|
||||
@@ -242,7 +246,7 @@
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
minWidth: 100,
|
||||
formatter: (row: any) => {
|
||||
return h(ElSwitch, {
|
||||
modelValue: row.status,
|
||||
@@ -259,13 +263,13 @@
|
||||
{
|
||||
prop: 'CreatedAt',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
minWidth: 180,
|
||||
formatter: (row: any) => formatDateTime(row.CreatedAt)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
formatter: (row: any) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
@@ -481,17 +485,22 @@
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const data = {
|
||||
role_name: form.role_name,
|
||||
role_desc: form.role_desc,
|
||||
role_type: form.role_type,
|
||||
status: form.status
|
||||
}
|
||||
|
||||
if (dialogType.value === 'add') {
|
||||
const data = {
|
||||
role_name: form.role_name,
|
||||
role_desc: form.role_desc,
|
||||
role_type: form.role_type,
|
||||
status: form.status
|
||||
}
|
||||
await RoleService.createRole(data)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
// 更新角色时只发送允许的字段
|
||||
const data = {
|
||||
role_name: form.role_name,
|
||||
role_desc: form.role_desc,
|
||||
status: form.status
|
||||
}
|
||||
await RoleService.updateRole(form.id, data)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
@@ -522,5 +531,4 @@
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user