feat:更换企业授权
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m41s

This commit is contained in:
luo
2026-06-22 10:36:33 +08:00
parent 866a2587b3
commit d3b5f55c64
34 changed files with 2478 additions and 3335 deletions

View File

@@ -297,8 +297,8 @@
v-model="usedDataForm.data_usage"
:min="0"
:max="usedDataMaxValue"
:precision="usedDataForm.unit === 'MB' ? 0 : 3"
:step="usedDataForm.unit === 'MB' ? 1 : 0.1"
:precision="3"
:step="0.1"
class="used-data-input__number"
placeholder="请输入新的套餐真实已用量"
/>
@@ -447,8 +447,8 @@
return
}
if (dataUsageMb === undefined || dataUsageMb < 0 || !Number.isInteger(dataUsageMb)) {
callback(new Error('请输入可换算为非负数 MB 的已用量'))
if (dataUsageMb === undefined || dataUsageMb < 0) {
callback(new Error('请输入可换算为非负数 MB 的已用量'))
return
}

View File

@@ -44,7 +44,6 @@
/>
<ElButton type="primary" @click="handleQuery">查询</ElButton>
<ElButton @click="handleReset()">重置</ElButton>
<ElTag type="info" size="small"> {{ total }} </ElTag>
</div>
</div>
</template>
@@ -66,6 +65,14 @@
<div v-else v-loading="loading" class="wallet-table-wrapper">
<ElTable v-if="hasTransactions" :data="transactionList" class="wallet-table" border>
<ElTableColumn prop="reference_no" label="业务编号">
<template #default="scope">
<span v-if="scope.row.reference_no" class="reference-no-link">
{{ scope.row.reference_no }}
</span>
<span v-else>--</span>
</template>
</ElTableColumn>
<ElTableColumn label="交易类型" width="100" align="center">
<template #default="scope">
<ElTag :type="getTransactionTypeTag(scope.row.transaction_type)" size="small">
@@ -103,14 +110,6 @@
<span v-else>--</span>
</template>
</ElTableColumn>
<ElTableColumn prop="reference_no" label="业务编号" min-width="180" show-overflow-tooltip>
<template #default="scope">
<span v-if="scope.row.reference_no" class="reference-no-link">
{{ scope.row.reference_no }}
</span>
<span v-else>--</span>
</template>
</ElTableColumn>
<ElTableColumn prop="remark" label="备注" min-width="150" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.remark || '-' }}

View File

@@ -44,17 +44,17 @@
</ElOption>
</ElSelect>
<template v-if="noSeriesId">
<div style=" margin-top: 4px; font-size: 12px;color: var(--el-text-color-warning)">
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-warning)">
该设备未关联套餐系列无法购买套餐
</div>
</template>
<template v-else-if="packagesEmptyWithSeriesId">
<div style=" margin-top: 4px; font-size: 12px;color: var(--el-text-color-info)">
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-info)">
该系列暂无可用套餐
</div>
</template>
<template v-else-if="selectedPackageIsGift">
<div style=" margin-top: 4px; font-size: 12px;color: var(--el-color-warning)">
<div style="margin-top: 4px; font-size: 12px; color: var(--el-color-warning)">
提示: 赠送套餐只能使用线下支付方式且必须上传支付凭证
</div>
</template>
@@ -83,23 +83,13 @@
label="支付凭证"
prop="payment_voucher_key"
>
<ElUpload
<VoucherUpload
ref="uploadRef"
class="payment-voucher-upload"
drag
:auto-upload="false"
:on-change="handleVoucherFileChange"
:on-remove="handleRemoveVoucher"
accept="image/*"
list-type="picture"
:limit="1"
>
<el-icon class="payment-voucher-upload__icon"><UploadFilled /></el-icon>
<div class="payment-voucher-upload__text"> 将支付凭证拖到此处<em>点击上传</em> </div>
<template #tip>
<div class="el-upload__tip">支持 jpgpng 格式文件大小不超过 5MB</div>
</template>
</ElUpload>
v-model="form.payment_voucher_key"
voucher-name="支付凭证"
@uploading-change="voucherUploading = $event"
@change="formRef?.validateField('payment_voucher_key')"
/>
</ElFormItem>
</ElForm>
<template #footer>
@@ -111,7 +101,7 @@
:loading="loading || voucherUploading"
:disabled="voucherUploading"
>
{{ voucherUploading ? '图片上传中...' : '提交' }}
{{ voucherUploading ? '凭证上传中...' : '提交' }}
</ElButton>
</div>
</template>
@@ -119,24 +109,22 @@
</template>
<script setup lang="ts">
import { computed, nextTick, reactive, ref, watch } from 'vue'
import { computed, reactive, ref, watch } from 'vue'
import {
ElButton,
ElDialog,
ElForm,
ElFormItem,
ElIcon,
ElMessage,
ElOption,
ElSelect,
ElTag,
ElUpload
ElTag
} from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus'
import { OrderService, PackageManageService, StorageService } from '@/api/modules'
import type { FormInstance, FormRules } from 'element-plus'
import { OrderService, PackageManageService } from '@/api/modules'
import type { CreateOrderRequest, PackageResponse } from '@/types/api'
import { useUserStore } from '@/store/modules/user'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
interface Props {
modelValue: boolean
@@ -156,13 +144,12 @@
const userStore = useUserStore()
const formRef = ref<FormInstance>()
const uploadRef = ref<UploadInstance>()
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
const loading = ref(false)
const voucherUploading = ref(false)
const packageSearchLoading = ref(false)
const packageOptions = ref<PackageResponse[]>([])
const packagesEmptyWithSeriesId = ref(false)
let activeVoucherUploadId = 0
const form = reactive({
package_id: undefined as number | undefined,
@@ -311,80 +298,6 @@
}
}
const invalidateVoucherUploadState = (clearFileKey = false) => {
activeVoucherUploadId += 1
voucherUploading.value = false
if (clearFileKey) {
form.payment_voucher_key = undefined
}
}
const handleVoucherFileChange = async (uploadFile: UploadFile) => {
const file = uploadFile.raw
if (!file) return
const isImage = file.type.startsWith('image/')
if (!isImage) {
ElMessage.error('只能上传图片文件!')
uploadRef.value?.clearFiles()
return
}
const isLt5M = file.size / 1024 / 1024 < 5
if (!isLt5M) {
ElMessage.error('图片大小不能超过 5MB!')
uploadRef.value?.clearFiles()
return
}
const requestId = ++activeVoucherUploadId
voucherUploading.value = true
form.payment_voucher_key = undefined
try {
ElMessage.info('正在上传支付凭证...')
const uploadUrlRes = await StorageService.getUploadUrl({
file_name: file.name,
content_type: file.type,
purpose: 'attachment'
})
if (uploadUrlRes.code !== 0) {
if (requestId !== activeVoucherUploadId) return
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
uploadRef.value?.clearFiles()
return
}
const { upload_url, file_key } = uploadUrlRes.data
await StorageService.uploadFile(upload_url, file, file.type)
if (requestId !== activeVoucherUploadId) return
form.payment_voucher_key = file_key
ElMessage.success('上传成功')
nextTick(() => {
formRef.value?.validateField('payment_voucher_key')
})
} catch (error: any) {
if (requestId !== activeVoucherUploadId) return
console.error('上传支付凭证失败:', error)
ElMessage.error(error?.message || '上传失败,请重试')
form.payment_voucher_key = undefined
uploadRef.value?.clearFiles()
} finally {
if (requestId === activeVoucherUploadId) {
voucherUploading.value = false
}
}
}
const handleRemoveVoucher = () => {
invalidateVoucherUploadState(true)
}
const handleCancel = () => {
visible.value = false
}
@@ -447,10 +360,11 @@
formRef.value?.resetFields()
form.package_id = undefined
form.payment_method = 'wallet'
invalidateVoucherUploadState(true)
form.payment_voucher_key = undefined
voucherUploading.value = false
packageOptions.value = []
packagesEmptyWithSeriesId.value = false
uploadRef.value?.clearFiles()
uploadRef.value?.clearFiles(false)
}
</script>
@@ -460,34 +374,4 @@
gap: 12px;
justify-content: flex-end;
}
.payment-voucher-upload {
width: 100%;
:deep(.el-upload) {
width: 100%;
}
:deep(.el-upload-dragger) {
width: 100%;
padding: 24px 16px;
border-radius: 8px;
}
&__icon {
margin-bottom: 12px;
font-size: 28px;
color: var(--el-color-primary);
}
&__text {
font-size: 14px;
color: var(--el-text-color-regular);
em {
font-style: normal;
color: var(--el-color-primary);
}
}
}
</style>

View File

@@ -5,7 +5,6 @@
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
label-width="120"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -41,6 +40,22 @@
>
批量设置套餐系列
</ElButton>
<ElButton
type="primary"
:disabled="!selectedDevices.length"
@click="showEnterpriseDeviceAuthorizeDialog"
v-permission="'devices:authorize_enterprise'"
>
授权设备给企业
</ElButton>
<ElButton
type="warning"
:disabled="!selectedDevices.length"
@click="showEnterpriseDeviceRecallDialog"
v-permission="'devices:recall_enterprise_authorization'"
>
撤销设备授权
</ElButton>
<ElButton type="primary" @click="showExportDialog" v-permission="'devices:export'">
导出
</ElButton>
@@ -677,6 +692,250 @@
:identifier="operationLogsIdentifier"
download-permission="device:download_log_file"
/>
<ElDialog
v-model="enterpriseDeviceAuthorizeDialogVisible"
title="授权设备给企业"
width="620px"
@close="handleEnterpriseDeviceAuthorizeDialogClose"
>
<ElForm
ref="enterpriseDeviceAuthorizeFormRef"
:model="enterpriseDeviceAuthorizeForm"
:rules="enterpriseDeviceAuthorizeRules"
label-width="120px"
>
<ElFormItem label="授权企业" prop="enterprise_id">
<ElSelect
v-model="enterpriseDeviceAuthorizeForm.enterprise_id"
placeholder="请选择或搜索企业"
clearable
filterable
remote
reserve-keyword
:remote-method="handleSearchEnterprise"
:loading="enterpriseLoading"
style="width: 100%"
>
<ElOption
v-for="item in enterpriseOptions"
:key="item.id"
:label="item.enterprise_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选设备方式" prop="selection_type">
<ElRadioGroup v-model="enterpriseDeviceAuthorizeForm.selection_type">
<ElRadio label="list">设备号列表</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="enterpriseDeviceAuthorizeForm.selection_type === DeviceSelectionType.LIST"
label="设备号列表"
>
<div>已选择 {{ selectedDevices.length }} 台设备</div>
</ElFormItem>
<template
v-if="enterpriseDeviceAuthorizeForm.selection_type === DeviceSelectionType.FILTER"
>
<ElFormItem label="设备号">
<ElInput
v-model="enterpriseDeviceAuthorizeForm.virtual_no"
placeholder="请输入设备号"
clearable
/>
</ElFormItem>
<ElFormItem label="批次号">
<ElSelect
v-model="enterpriseDeviceAuthorizeForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in searchBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="店铺名称">
<ElCascader
v-model="enterpriseDeviceAuthorizeForm.shop_id_path"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择店铺名称"
clearable
filterable
style="width: 100%"
/>
</ElFormItem>
</template>
<ElFormItem label="备注">
<ElInput
v-model="enterpriseDeviceAuthorizeForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="enterpriseDeviceAuthorizeDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleEnterpriseDeviceAuthorize"
:loading="enterpriseDeviceAuthorizeLoading"
>
确认授权
</ElButton>
</div>
</template>
</ElDialog>
<ElDialog
v-model="enterpriseDeviceRecallDialogVisible"
title="撤销设备授权"
width="620px"
@close="handleEnterpriseDeviceRecallDialogClose"
>
<ElForm
ref="enterpriseDeviceRecallFormRef"
:model="enterpriseDeviceRecallForm"
:rules="enterpriseDeviceRecallRules"
label-width="120px"
>
<ElFormItem label="授权企业" prop="enterprise_id">
<ElSelect
v-model="enterpriseDeviceRecallForm.enterprise_id"
placeholder="请选择或搜索企业"
clearable
filterable
remote
reserve-keyword
:remote-method="handleSearchEnterprise"
:loading="enterpriseLoading"
style="width: 100%"
>
<ElOption
v-for="item in enterpriseOptions"
:key="item.id"
:label="item.enterprise_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选设备方式" prop="selection_type">
<ElRadioGroup v-model="enterpriseDeviceRecallForm.selection_type">
<ElRadio label="list">设备号列表</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="enterpriseDeviceRecallForm.selection_type === DeviceSelectionType.LIST"
label="设备号列表"
>
<div>已选择 {{ selectedDevices.length }} 台设备</div>
</ElFormItem>
<template
v-if="enterpriseDeviceRecallForm.selection_type === DeviceSelectionType.FILTER"
>
<ElFormItem label="设备号">
<ElInput
v-model="enterpriseDeviceRecallForm.virtual_no"
placeholder="请输入设备号"
clearable
/>
</ElFormItem>
<ElFormItem label="批次号">
<ElSelect
v-model="enterpriseDeviceRecallForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in searchBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</ElFormItem>
</template>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="enterpriseDeviceRecallDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleEnterpriseDeviceRecall"
:loading="enterpriseDeviceRecallLoading"
>
确认撤销
</ElButton>
</div>
</template>
</ElDialog>
<ElDialog
v-model="enterpriseDeviceResultDialogVisible"
:title="enterpriseDeviceResultTitle"
width="700px"
>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="成功数">
<ElTag type="success">{{ enterpriseDeviceOperationResult.success_count }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="失败数">
<ElTag type="danger">{{ enterpriseDeviceOperationResult.fail_count }}</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
<div
v-if="
enterpriseDeviceOperationResult.failed_items &&
enterpriseDeviceOperationResult.failed_items.length > 0
"
style="margin-top: 20px"
>
<ElDivider content-position="left">失败项详情</ElDivider>
<ElTable :data="enterpriseDeviceOperationResult.failed_items" border max-height="300">
<ElTableColumn prop="virtual_no" label="设备号" width="180" />
<ElTableColumn prop="reason" label="失败原因" />
</ElTable>
</div>
<div
v-if="
enterpriseDeviceOperationResult.authorized_devices &&
enterpriseDeviceOperationResult.authorized_devices.length > 0
"
style="margin-top: 20px"
>
<ElDivider content-position="left">已授权设备</ElDivider>
<ElTable
:data="enterpriseDeviceOperationResult.authorized_devices"
border
max-height="220"
>
<ElTableColumn prop="virtual_no" label="设备号" width="180" />
<ElTableColumn prop="card_count" label="绑定卡数" />
</ElTable>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="enterpriseDeviceResultDialogVisible = false"
>确定</ElButton
>
</div>
</template>
</ElDialog>
<ExportTaskCreateDialog
v-model="exportDialogVisible"
scene="device"
@@ -699,7 +958,7 @@
CardService,
PackageSeriesService,
AssetService,
CarrierService
EnterpriseService
} from '@/api/modules'
import { DeviceSelectionType } from '@/types/api'
import { ElMessage, ElMessageBox, ElTag, ElIcon, ElRadioGroup, ElRadio } from 'element-plus'
@@ -720,6 +979,11 @@
import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format'
import type { PackageSeriesResponse } from '@/types/api'
import type { EnterpriseItem } from '@/types/api/enterprise'
import type {
EnterpriseAllocateDevicesResponse,
EnterpriseRecallDevicesResponse
} from '@/types/api/enterpriseDevice'
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
import OperationLogsDialog from '@/components/business/OperationLogsDialog.vue'
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
@@ -781,6 +1045,20 @@
label: 'label',
children: 'children'
}
const loadTopLevelShopCascadeOptions = async () => {
try {
const res = await ShopService.getShopsCascade({ parent_id: undefined })
if (res.code === 0) {
shopCascadeOptions.value = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('加载顶级店铺失败:', error)
}
}
const seriesBindingShopCascadeOptions = ref<any[]>([])
const seriesBindingShopCascadeProps = {
lazy: true,
@@ -815,6 +1093,27 @@
}
const allocateResult = ref<AllocateDevicesResponse | null>(null)
const recallResult = ref<RecallDevicesResponse | null>(null)
const enterpriseDeviceAuthorizeDialogVisible = ref(false)
const enterpriseDeviceRecallDialogVisible = ref(false)
const enterpriseDeviceResultDialogVisible = ref(false)
const enterpriseDeviceAuthorizeLoading = ref(false)
const enterpriseDeviceRecallLoading = ref(false)
const enterpriseDeviceResultTitle = ref('')
const enterpriseDeviceAuthorizeFormRef = ref<FormInstance>()
const enterpriseDeviceRecallFormRef = ref<FormInstance>()
const enterpriseOptions = ref<EnterpriseItem[]>([])
const enterpriseLoading = ref(false)
const enterpriseDeviceOperationResult = ref<
Partial<EnterpriseAllocateDevicesResponse & EnterpriseRecallDevicesResponse> & {
success_count: number
fail_count: number
}
>({
success_count: 0,
fail_count: 0,
failed_items: null,
authorized_devices: null
})
// 套餐系列绑定相关
const seriesBindingDialogVisible = ref(false)
@@ -840,6 +1139,22 @@
created_at_end: ''
})
const seriesBindingForm = reactive(createInitialSeriesBindingState())
const createInitialEnterpriseDeviceAuthorizeState = () => ({
enterprise_id: undefined as number | undefined,
selection_type: DeviceSelectionType.LIST,
virtual_no: '',
batch_no: '',
shop_id_path: undefined as number[] | undefined,
remark: ''
})
const createInitialEnterpriseDeviceRecallState = () => ({
enterprise_id: undefined as number | undefined,
selection_type: DeviceSelectionType.LIST,
virtual_no: '',
batch_no: ''
})
const enterpriseDeviceAuthorizeForm = reactive(createInitialEnterpriseDeviceAuthorizeState())
const enterpriseDeviceRecallForm = reactive(createInitialEnterpriseDeviceRecallState())
const parseNumericStringToBigInt = (value: string): bigint | null => {
const normalizedValue = value.trim()
if (!normalizedValue || !/^\d+$/.test(normalizedValue)) {
@@ -931,6 +1246,13 @@
}
]
})
const createEnterpriseDeviceRules = () =>
reactive<FormRules>({
enterprise_id: [{ required: true, message: '请选择企业', trigger: 'change' }],
selection_type: [{ required: true, message: '请选择选设备方式', trigger: 'change' }]
})
const enterpriseDeviceAuthorizeRules = createEnterpriseDeviceRules()
const enterpriseDeviceRecallRules = createEnterpriseDeviceRules()
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
// 搜索表单店铺/系列选项
@@ -999,13 +1321,15 @@
shop_id: undefined as number | undefined,
series_id: undefined as number | undefined,
has_active_package: undefined as boolean | undefined,
authorized_enterprise_id: undefined as number | undefined,
is_authorized_to_enterprise: undefined as boolean | undefined,
dateRange: [] as string[],
created_at_start: '',
created_at_end: ''
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
const searchForm = reactive<Record<string, any>>({ ...initialSearchState })
// 搜索批次号(用于搜索表单)
const loadSearchBatchNoOptions = async (batchNo?: string) => {
@@ -1174,7 +1498,7 @@
}))
},
{
label: '起始至结束',
label: '起止时间',
prop: 'dateRange',
type: 'date',
config: {
@@ -1189,7 +1513,7 @@
// 分页
searchFormItems.splice(searchFormItems.length - 1, 0, {
label: '是否有生效套餐',
label: '效套餐',
prop: 'has_active_package',
type: 'select',
config: {
@@ -1201,6 +1525,37 @@
{ label: '否', value: false }
]
})
searchFormItems.splice(searchFormItems.length - 1, 0, {
label: '授权企业',
prop: 'authorized_enterprise_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => handleSearchEnterprise(query),
loading: enterpriseLoading.value,
placeholder: '请选择或搜索企业'
},
options: () =>
enterpriseOptions.value.map((enterprise) => ({
label: enterprise.enterprise_name,
value: enterprise.id
}))
})
searchFormItems.splice(searchFormItems.length - 1, 0, {
label: '授权状态',
prop: 'is_authorized_to_enterprise',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '已授权', value: true },
{ label: '未授权', value: false }
]
})
const pagination = reactive({
page: 1,
@@ -1704,6 +2059,26 @@
}
}
const handleSearchEnterprise = async (query?: string) => {
try {
enterpriseLoading.value = true
const params = {
page: 1,
page_size: 20,
enterprise_name: query || undefined,
status: 1
}
const res = await EnterpriseService.getEnterprises(params)
if (res.code === 0) {
enterpriseOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索企业失败:', error)
} finally {
enterpriseLoading.value = false
}
}
let isFirstActivation = true
onMounted(() => {
getTableData()
@@ -1735,6 +2110,8 @@
shop_id: searchForm.shop_id || undefined,
series_id: searchForm.series_id || undefined,
has_active_package: searchForm.has_active_package ?? undefined,
authorized_enterprise_id: searchForm.authorized_enterprise_id || undefined,
is_authorized_to_enterprise: searchForm.is_authorized_to_enterprise ?? undefined,
created_at_start: searchForm.created_at_start || undefined,
created_at_end: searchForm.created_at_end || undefined
}
@@ -1763,6 +2140,8 @@
shop_id: searchForm.shop_id || undefined,
series_id: searchForm.series_id || undefined,
has_active_package: searchForm.has_active_package ?? undefined,
authorized_enterprise_id: searchForm.authorized_enterprise_id || undefined,
is_authorized_to_enterprise: searchForm.is_authorized_to_enterprise ?? undefined,
created_at_start: searchForm.created_at_start || undefined,
created_at_end: searchForm.created_at_end || undefined
}
@@ -1993,6 +2372,160 @@
recallForm.remark = ''
}
const getInferredDeviceEnterpriseId = () => {
const filterEnterpriseId = searchForm.authorized_enterprise_id
if (filterEnterpriseId) return filterEnterpriseId
const selectedEnterpriseIds = Array.from(
new Set(
selectedDevices.value
.map((device) => device.authorized_enterprise_id)
.filter((id): id is number => typeof id === 'number')
)
)
return selectedEnterpriseIds.length === 1 ? selectedEnterpriseIds[0] : undefined
}
const resetEnterpriseDeviceAuthorizeForm = () => {
Object.assign(enterpriseDeviceAuthorizeForm, createInitialEnterpriseDeviceAuthorizeState(), {
selection_type:
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER,
virtual_no: searchForm.virtual_no || '',
batch_no: searchForm.batch_no || '',
shop_id_path: undefined
})
}
const resetEnterpriseDeviceRecallForm = () => {
Object.assign(enterpriseDeviceRecallForm, createInitialEnterpriseDeviceRecallState(), {
enterprise_id: getInferredDeviceEnterpriseId(),
selection_type:
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER,
virtual_no: searchForm.virtual_no || '',
batch_no: searchForm.batch_no || ''
})
}
const showEnterpriseDeviceAuthorizeDialog = async () => {
resetEnterpriseDeviceAuthorizeForm()
await Promise.all([
handleSearchEnterprise(),
loadSearchBatchNoOptions(),
loadTopLevelShopCascadeOptions()
])
enterpriseDeviceAuthorizeDialogVisible.value = true
enterpriseDeviceAuthorizeFormRef.value?.clearValidate()
}
const showEnterpriseDeviceRecallDialog = async () => {
resetEnterpriseDeviceRecallForm()
await Promise.all([handleSearchEnterprise(), loadSearchBatchNoOptions()])
enterpriseDeviceRecallDialogVisible.value = true
enterpriseDeviceRecallFormRef.value?.clearValidate()
}
const handleEnterpriseDeviceAuthorizeDialogClose = () => {
Object.assign(enterpriseDeviceAuthorizeForm, createInitialEnterpriseDeviceAuthorizeState())
enterpriseDeviceAuthorizeFormRef.value?.resetFields()
}
const handleEnterpriseDeviceRecallDialogClose = () => {
Object.assign(enterpriseDeviceRecallForm, createInitialEnterpriseDeviceRecallState())
enterpriseDeviceRecallFormRef.value?.resetFields()
}
const buildEnterpriseDeviceParams = (form: any) => {
const params: Record<string, unknown> = {
selection_type: form.selection_type,
remark: form.remark || undefined
}
if (form.selection_type === DeviceSelectionType.LIST) {
const deviceNos = selectedDevices.value.map((device) => device.virtual_no).filter(Boolean)
if (!deviceNos.length) {
ElMessage.warning('列表模式下请先选择设备')
return null
}
params.device_nos = deviceNos
} else {
if (form.batch_no) params.batch_no = form.batch_no
if (Array.isArray(form.shop_id_path) && form.shop_id_path.length > 0) {
params.shop_id = form.shop_id_path[form.shop_id_path.length - 1]
}
if (form.virtual_no) params.virtual_no = form.virtual_no
}
Object.keys(params).forEach((key) => {
if (params[key] === undefined || params[key] === '') delete params[key]
})
return params
}
const applyEnterpriseDeviceOperationSuccess = async (
title: string,
result: EnterpriseAllocateDevicesResponse | EnterpriseRecallDevicesResponse
) => {
enterpriseDeviceOperationResult.value = result
enterpriseDeviceResultTitle.value = title
enterpriseDeviceResultDialogVisible.value = true
selectedDevices.value = []
await getTableData()
}
const handleEnterpriseDeviceAuthorize = async () => {
if (!enterpriseDeviceAuthorizeFormRef.value) return
await enterpriseDeviceAuthorizeFormRef.value.validate(async (valid) => {
if (!valid) return
const params = buildEnterpriseDeviceParams(enterpriseDeviceAuthorizeForm)
if (!params) return
enterpriseDeviceAuthorizeLoading.value = true
try {
const res = await EnterpriseService.allocateDevices(
enterpriseDeviceAuthorizeForm.enterprise_id!,
params as any
)
if (res.code === 0) {
enterpriseDeviceAuthorizeDialogVisible.value = false
await applyEnterpriseDeviceOperationSuccess('授权设备给企业结果', res.data)
}
} catch (error) {
console.error('授权设备给企业失败:', error)
} finally {
enterpriseDeviceAuthorizeLoading.value = false
}
})
}
const handleEnterpriseDeviceRecall = async () => {
if (!enterpriseDeviceRecallFormRef.value) return
await enterpriseDeviceRecallFormRef.value.validate(async (valid) => {
if (!valid) return
const params = buildEnterpriseDeviceParams(enterpriseDeviceRecallForm)
if (!params) return
enterpriseDeviceRecallLoading.value = true
try {
const res = await EnterpriseService.recallDevices(
enterpriseDeviceRecallForm.enterprise_id!,
params as any
)
if (res.code === 0) {
enterpriseDeviceRecallDialogVisible.value = false
await applyEnterpriseDeviceOperationSuccess('撤销设备授权结果', res.data)
}
} catch (error) {
console.error('撤销设备授权失败:', error)
} finally {
enterpriseDeviceRecallLoading.value = false
}
})
}
// 批量设置套餐系列
const handleBatchSetSeries = async () => {
seriesBindingResult.value = null

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
label-width="120"
:label-width="90"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -42,6 +42,22 @@
>
批量设置套餐系列
</ElButton>
<ElButton
type="primary"
:disabled="selectedCards.length === 0"
@click="showEnterpriseCardAuthorizeDialog"
v-permission="'iot_card:authorize_enterprise'"
>
授权卡给企业
</ElButton>
<ElButton
type="warning"
:disabled="selectedCards.length === 0"
@click="showEnterpriseCardRecallDialog"
v-permission="'iot_card:recall_enterprise_authorization'"
>
回收卡授权
</ElButton>
<ElButton type="primary" @click="showExportDialog" v-permission="'iot_card:export'">
导出
</ElButton>
@@ -357,6 +373,350 @@
</template>
</ElDialog>
<!-- 企业卡授权对话框 -->
<ElDialog
v-model="enterpriseCardAuthorizeDialogVisible"
title="授权卡给企业"
width="640px"
@close="handleEnterpriseCardAuthorizeDialogClose"
>
<ElForm
ref="enterpriseCardAuthorizeFormRef"
:model="enterpriseCardAuthorizeForm"
:rules="enterpriseCardAuthorizeRules"
label-width="120px"
>
<ElFormItem label="授权企业" prop="enterprise_id">
<ElSelect
v-model="enterpriseCardAuthorizeForm.enterprise_id"
placeholder="请选择或搜索企业"
clearable
filterable
remote
reserve-keyword
:remote-method="handleSearchEnterprise"
:loading="enterpriseLoading"
style="width: 100%"
>
<ElOption
v-for="item in enterpriseOptions"
:key="item.id"
:label="item.enterprise_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="enterpriseCardAuthorizeForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="enterpriseCardAuthorizeForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="enterpriseCardAuthorizeForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElInput
v-model="enterpriseCardAuthorizeForm.iccid_start"
placeholder="请输入起始ICCID"
/>
</ElFormItem>
<ElFormItem
v-if="enterpriseCardAuthorizeForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElInput
v-model="enterpriseCardAuthorizeForm.iccid_end"
placeholder="请输入结束ICCID"
/>
</ElFormItem>
<div
v-if="
enterpriseCardAuthorizeForm.selection_type === CardSelectionType.RANGE &&
enterpriseCardAuthorizeRangeCountText
"
class="range-allocation-hint range-allocation-hint--card-form"
>
共授权{{ enterpriseCardAuthorizeRangeCountText }}张卡
</div>
<template
v-if="enterpriseCardAuthorizeForm.selection_type === CardSelectionType.FILTER"
>
<ElFormItem label="运营商">
<ElSelect
v-model="enterpriseCardAuthorizeForm.carrier_id"
placeholder="请选择或搜索运营商"
clearable
filterable
remote
:remote-method="handleSearchAllocateCarrier"
:loading="allocateCarrierLoading"
style="width: 100%"
>
<ElOption
v-for="item in allocateCarrierOptions"
:key="item.id"
:label="item.carrier_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="店铺">
<ElCascader
v-model="enterpriseCardAuthorizeForm.shop_id_path"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择店铺"
style="width: 100%"
clearable
filterable
/>
</ElFormItem>
<ElFormItem label="批次号">
<ElSelect
v-model="enterpriseCardAuthorizeForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchAllocateBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in allocateBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="ICCID">
<ElInput v-model="enterpriseCardAuthorizeForm.iccid" placeholder="请输入ICCID" />
</ElFormItem>
</template>
<ElFormItem label="备注">
<ElInput
v-model="enterpriseCardAuthorizeForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="enterpriseCardAuthorizeDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleEnterpriseCardAuthorize"
:loading="enterpriseCardAuthorizeLoading"
>
确认授权
</ElButton>
</div>
</template>
</ElDialog>
<!-- 企业卡授权回收对话框 -->
<ElDialog
v-model="enterpriseCardRecallDialogVisible"
title="回收卡授权"
width="640px"
@close="handleEnterpriseCardRecallDialogClose"
>
<ElForm
ref="enterpriseCardRecallFormRef"
:model="enterpriseCardRecallForm"
:rules="enterpriseCardRecallRules"
label-width="120px"
>
<ElFormItem label="授权企业" prop="enterprise_id">
<ElSelect
v-model="enterpriseCardRecallForm.enterprise_id"
placeholder="请选择或搜索企业"
clearable
filterable
remote
reserve-keyword
:remote-method="handleSearchEnterprise"
:loading="enterpriseLoading"
style="width: 100%"
>
<ElOption
v-for="item in enterpriseOptions"
:key="item.id"
:label="item.enterprise_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="enterpriseCardRecallForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="enterpriseCardRecallForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="enterpriseCardRecallForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElInput
v-model="enterpriseCardRecallForm.iccid_start"
placeholder="请输入起始ICCID"
/>
</ElFormItem>
<ElFormItem
v-if="enterpriseCardRecallForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElInput v-model="enterpriseCardRecallForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<div
v-if="
enterpriseCardRecallForm.selection_type === CardSelectionType.RANGE &&
enterpriseCardRecallRangeCountText
"
class="range-allocation-hint range-allocation-hint--card-form"
>
共回收{{ enterpriseCardRecallRangeCountText }}张卡
</div>
<template v-if="enterpriseCardRecallForm.selection_type === CardSelectionType.FILTER">
<ElFormItem label="运营商">
<ElSelect
v-model="enterpriseCardRecallForm.carrier_id"
placeholder="请选择或搜索运营商"
clearable
filterable
remote
:remote-method="handleSearchAllocateCarrier"
:loading="allocateCarrierLoading"
style="width: 100%"
>
<ElOption
v-for="item in allocateCarrierOptions"
:key="item.id"
:label="item.carrier_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="批次号">
<ElSelect
v-model="enterpriseCardRecallForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchAllocateBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in allocateBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="ICCID">
<ElInput v-model="enterpriseCardRecallForm.iccid" placeholder="请输入ICCID" />
</ElFormItem>
</template>
<ElFormItem label="备注">
<ElInput
v-model="enterpriseCardRecallForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="enterpriseCardRecallDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleEnterpriseCardRecall"
:loading="enterpriseCardRecallLoading"
>
确认回收
</ElButton>
</div>
</template>
</ElDialog>
<!-- 企业卡授权操作结果 -->
<ElDialog
v-model="enterpriseCardResultDialogVisible"
:title="enterpriseCardResultTitle"
width="700px"
>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="成功数">
<ElTag type="success">{{ enterpriseCardOperationResult.success_count }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="失败数">
<ElTag type="danger">{{ enterpriseCardOperationResult.fail_count }}</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
<div
v-if="
enterpriseCardOperationResult.failed_items &&
enterpriseCardOperationResult.failed_items.length > 0
"
style="margin-top: 20px"
>
<ElDivider content-position="left">失败项详情</ElDivider>
<ElTable :data="enterpriseCardOperationResult.failed_items" border max-height="300">
<ElTableColumn prop="iccid" label="ICCID" width="200" />
<ElTableColumn prop="reason" label="失败原因" />
</ElTable>
</div>
<div
v-if="
enterpriseCardOperationResult.recalled_devices &&
enterpriseCardOperationResult.recalled_devices.length > 0
"
style="margin-top: 20px"
>
<ElDivider content-position="left">连带回收设备</ElDivider>
<ElTable :data="enterpriseCardOperationResult.recalled_devices" border max-height="220">
<ElTableColumn prop="virtual_no" label="设备虚拟号" width="180" />
<ElTableColumn prop="card_count" label="关联卡数" width="100" />
<ElTableColumn label="ICCID列表">
<template #default="{ row }">
{{ row.iccids?.join(', ') || '-' }}
</template>
</ElTableColumn>
</ElTable>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="enterpriseCardResultDialogVisible = false"
>确定</ElButton
>
</div>
</template>
</ElDialog>
<!-- 批量设置套餐系列绑定对话框 -->
<ElDialog
v-model="seriesBindingDialogVisible"
@@ -652,7 +1012,8 @@
ShopService,
PackageSeriesService,
AssetService,
CarrierService
CarrierService,
EnterpriseService
} from '@/api/modules'
import { ElMessage, ElTag, ElIcon, ElMessageBox } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
@@ -677,6 +1038,8 @@
} from '@/types/api/card'
import { CardSelectionType } from '@/types/api/card'
import type { PackageSeriesResponse } from '@/types/api'
import type { EnterpriseItem } from '@/types/api/enterprise'
import type { AllocateCardsResponse, RecallCardsResponse } from '@/types/api/enterpriseCard'
import OperationLogsDialog from '@/components/business/OperationLogsDialog.vue'
defineOptions({ name: 'StandaloneCardList' })
@@ -704,6 +1067,27 @@
fail_count: 0,
failed_items: null
})
const enterpriseCardAuthorizeDialogVisible = ref(false)
const enterpriseCardRecallDialogVisible = ref(false)
const enterpriseCardResultDialogVisible = ref(false)
const enterpriseCardAuthorizeLoading = ref(false)
const enterpriseCardRecallLoading = ref(false)
const enterpriseCardResultTitle = ref('')
const enterpriseCardAuthorizeFormRef = ref<FormInstance>()
const enterpriseCardRecallFormRef = ref<FormInstance>()
const enterpriseOptions = ref<EnterpriseItem[]>([])
const enterpriseLoading = ref(false)
const enterpriseCardOperationResult = ref<
Partial<AllocateCardsResponse & RecallCardsResponse> & {
success_count: number
fail_count: number
}
>({
success_count: 0,
fail_count: 0,
failed_items: null,
recalled_devices: null
})
// 操作审计日志弹窗
const operationLogsDialogVisible = ref(false)
@@ -727,6 +1111,29 @@
batch_no: ''
})
const seriesBindingForm = reactive(createInitialSeriesBindingState())
const createInitialEnterpriseCardAuthorizeState = () => ({
enterprise_id: undefined as number | undefined,
selection_type: CardSelectionType.LIST,
iccid_start: '',
iccid_end: '',
carrier_id: undefined as number | undefined,
shop_id_path: undefined as number[] | undefined,
batch_no: '',
iccid: '',
remark: ''
})
const createInitialEnterpriseCardRecallState = () => ({
enterprise_id: undefined as number | undefined,
selection_type: CardSelectionType.LIST,
iccid_start: '',
iccid_end: '',
carrier_id: undefined as number | undefined,
batch_no: '',
iccid: '',
remark: ''
})
const enterpriseCardAuthorizeForm = reactive(createInitialEnterpriseCardAuthorizeState())
const enterpriseCardRecallForm = reactive(createInitialEnterpriseCardRecallState())
const parseNumericStringToBigInt = (value: string): bigint | null => {
const normalizedValue = value.trim()
if (!normalizedValue || !/^\d+$/.test(normalizedValue)) {
@@ -761,9 +1168,54 @@
const recallRangeCountText = computed(() =>
getFormattedRangeCountText(recallForm.iccid_start, recallForm.iccid_end)
)
const enterpriseCardAuthorizeRangeCountText = computed(() =>
getFormattedRangeCountText(
enterpriseCardAuthorizeForm.iccid_start,
enterpriseCardAuthorizeForm.iccid_end
)
)
const enterpriseCardRecallRangeCountText = computed(() =>
getFormattedRangeCountText(
enterpriseCardRecallForm.iccid_start,
enterpriseCardRecallForm.iccid_end
)
)
const seriesBindingRangeCountText = computed(() =>
getFormattedRangeCountText(seriesBindingForm.iccid_start, seriesBindingForm.iccid_end)
)
const createEnterpriseCardRules = (form: { selection_type: CardSelectionType }) =>
reactive<FormRules>({
enterprise_id: [{ required: true, message: '请选择企业', trigger: 'change' }],
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
iccid_start: [
{
required: true,
validator: (_rule, value, callback) => {
if (form.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入起始ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
],
iccid_end: [
{
required: true,
validator: (_rule, value, callback) => {
if (form.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入结束ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
const enterpriseCardAuthorizeRules = createEnterpriseCardRules(enterpriseCardAuthorizeForm)
const enterpriseCardRecallRules = createEnterpriseCardRules(enterpriseCardRecallForm)
const seriesBindingRules = reactive<FormRules>({
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
@@ -921,6 +1373,26 @@
}
}
const handleSearchEnterprise = async (query?: string) => {
try {
enterpriseLoading.value = true
const params = {
page: 1,
page_size: 20,
enterprise_name: query || undefined,
status: 1
}
const res = await EnterpriseService.getEnterprises(params)
if (res.code === 0) {
enterpriseOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索企业失败:', error)
} finally {
enterpriseLoading.value = false
}
}
// 搜索套餐系列(用于搜索栏)
const handleSearchSeries = async (query: string) => {
try {
@@ -1043,10 +1515,25 @@
label: 'label',
children: 'children'
}
const loadTopLevelShopCascadeOptions = async () => {
try {
const res = await ShopService.getShopsCascade({ parent_id: undefined })
if (res.code === 0) {
shopCascadeOptions.value = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('加载顶级店铺失败:', error)
}
}
// 搜索表单初始值
const initialSearchState: {
status: undefined | number
network_status: undefined | number
iccid: string
carrier_name: string
msisdn: string
@@ -1057,9 +1544,12 @@
has_active_package: undefined | boolean
is_distributed: undefined | number
is_standalone: undefined | boolean
authorized_enterprise_id: undefined | number
is_authorized_to_enterprise: undefined | boolean
[key: string]: any
} = {
status: undefined,
network_status: undefined,
iccid: '',
carrier_name: '',
msisdn: '',
@@ -1070,6 +1560,8 @@
has_active_package: undefined,
is_distributed: undefined,
is_standalone: undefined,
authorized_enterprise_id: undefined,
is_authorized_to_enterprise: undefined,
is_replaced: undefined,
iccid_start: '',
iccid_end: ''
@@ -1184,7 +1676,7 @@
}
},
{
label: '运营商名称',
label: '运营商',
prop: 'carrier_name',
type: 'select',
config: {
@@ -1216,6 +1708,19 @@
{ label: '已停用', value: 4 }
]
},
{
label: '网络状态',
prop: 'network_status',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '停机', value: 0 },
{ label: '正常', value: 1 }
]
},
{
label: '卡虚拟号',
prop: 'virtual_no',
@@ -1235,7 +1740,7 @@
}
},
{
label: '绑定设备虚拟号',
label: '设备虚拟号',
prop: 'device_virtual_no',
type: 'input',
config: {
@@ -1279,7 +1784,7 @@
}))
},
{
label: '是否已分销',
label: '分销状态',
prop: 'is_distributed',
type: 'select',
config: {
@@ -1292,7 +1797,7 @@
]
},
{
label: '是否绑设备',
label: '是否绑设备',
prop: 'is_standalone',
type: 'select',
config: {
@@ -1305,7 +1810,7 @@
]
},
{
label: '是否有换卡记录',
label: '有无换卡',
prop: 'is_replaced',
type: 'select',
config: {
@@ -1318,7 +1823,7 @@
]
},
{
label: 'ICCID起始号',
label: '起始号',
prop: 'iccid_start',
type: 'input',
config: {
@@ -1327,7 +1832,7 @@
}
},
{
label: 'ICCID结束号',
label: '结束号',
prop: 'iccid_end',
type: 'input',
config: {
@@ -1339,7 +1844,7 @@
// 搜索表单配置
baseFormItems.splice(8, 0, {
label: '是否有生效套餐',
label: '效套餐',
prop: 'has_active_package',
type: 'select',
config: {
@@ -1351,6 +1856,37 @@
{ label: '否', value: false }
]
})
baseFormItems.splice(9, 0, {
label: '授权企业',
prop: 'authorized_enterprise_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchEnterprise,
loading: enterpriseLoading.value,
placeholder: '请选择或搜索企业'
},
options: () =>
enterpriseOptions.value.map((enterprise) => ({
label: enterprise.enterprise_name,
value: enterprise.id
}))
})
baseFormItems.splice(10, 0, {
label: '授权状态',
prop: 'is_authorized_to_enterprise',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '已授权', value: true },
{ label: '未授权', value: false }
]
})
const formItems: SearchFormItem[] = baseFormItems
@@ -1999,6 +2535,175 @@
})
}
const getInferredCardEnterpriseId = () => {
const filterEnterpriseId = formFilters.authorized_enterprise_id
if (filterEnterpriseId) return filterEnterpriseId
const selectedEnterpriseIds = Array.from(
new Set(
selectedCards.value
.map((card) => card.authorized_enterprise_id)
.filter((id): id is number => typeof id === 'number')
)
)
return selectedEnterpriseIds.length === 1 ? selectedEnterpriseIds[0] : undefined
}
const resetEnterpriseCardAuthorizeForm = () => {
Object.assign(enterpriseCardAuthorizeForm, createInitialEnterpriseCardAuthorizeState(), {
selection_type:
selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER,
carrier_id: undefined,
shop_id_path: undefined,
batch_no: formFilters.batch_no || '',
iccid: formFilters.iccid || ''
})
}
const resetEnterpriseCardRecallForm = () => {
Object.assign(enterpriseCardRecallForm, createInitialEnterpriseCardRecallState(), {
enterprise_id: getInferredCardEnterpriseId(),
selection_type:
selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER,
carrier_id: undefined,
batch_no: formFilters.batch_no || '',
iccid: formFilters.iccid || ''
})
}
const showEnterpriseCardAuthorizeDialog = async () => {
resetEnterpriseCardAuthorizeForm()
await Promise.all([
handleSearchEnterprise(),
loadAllocateCarrierOptions(),
loadAllocateBatchNoOptions(),
loadTopLevelShopCascadeOptions()
])
enterpriseCardAuthorizeDialogVisible.value = true
enterpriseCardAuthorizeFormRef.value?.clearValidate()
}
const showEnterpriseCardRecallDialog = async () => {
resetEnterpriseCardRecallForm()
await Promise.all([
handleSearchEnterprise(),
loadAllocateCarrierOptions(),
loadAllocateBatchNoOptions()
])
enterpriseCardRecallDialogVisible.value = true
enterpriseCardRecallFormRef.value?.clearValidate()
}
const handleEnterpriseCardAuthorizeDialogClose = () => {
Object.assign(enterpriseCardAuthorizeForm, createInitialEnterpriseCardAuthorizeState())
enterpriseCardAuthorizeFormRef.value?.resetFields()
}
const handleEnterpriseCardRecallDialogClose = () => {
Object.assign(enterpriseCardRecallForm, createInitialEnterpriseCardRecallState())
enterpriseCardRecallFormRef.value?.resetFields()
}
const appendEnterpriseCardFilterParams = (params: Record<string, unknown>, form: any) => {
if (form.carrier_id) params.carrier_id = form.carrier_id
if (Array.isArray(form.shop_id_path) && form.shop_id_path.length > 0) {
params.shop_id = form.shop_id_path[form.shop_id_path.length - 1]
}
if (form.batch_no) params.batch_no = form.batch_no
if (form.iccid) params.iccid = form.iccid
}
const buildEnterpriseCardParams = (form: any) => {
const params: Record<string, unknown> = {
selection_type: form.selection_type,
remark: form.remark || undefined
}
if (form.selection_type === CardSelectionType.LIST) {
const iccids = selectedCards.value.map((card) => card.iccid)
if (!iccids.length) {
ElMessage.warning('列表模式下请先选择卡')
return null
}
params.iccids = iccids
} else if (form.selection_type === CardSelectionType.RANGE) {
params.iccid_start = form.iccid_start
params.iccid_end = form.iccid_end
} else {
appendEnterpriseCardFilterParams(params, form)
}
Object.keys(params).forEach((key) => {
if (params[key] === undefined || params[key] === '') delete params[key]
})
return params
}
const applyEnterpriseCardOperationSuccess = async (
title: string,
result: AllocateCardsResponse | RecallCardsResponse
) => {
enterpriseCardOperationResult.value = result
enterpriseCardResultTitle.value = title
enterpriseCardResultDialogVisible.value = true
selectedCards.value = []
await getTableData()
}
const handleEnterpriseCardAuthorize = async () => {
if (!enterpriseCardAuthorizeFormRef.value) return
await enterpriseCardAuthorizeFormRef.value.validate(async (valid) => {
if (!valid) return
const params = buildEnterpriseCardParams(enterpriseCardAuthorizeForm)
if (!params) return
enterpriseCardAuthorizeLoading.value = true
try {
const res = await EnterpriseService.allocateCards(
enterpriseCardAuthorizeForm.enterprise_id!,
params as any
)
if (res.code === 0) {
enterpriseCardAuthorizeDialogVisible.value = false
await applyEnterpriseCardOperationSuccess('授权卡给企业结果', res.data)
}
} catch (error) {
console.error('授权卡给企业失败:', error)
} finally {
enterpriseCardAuthorizeLoading.value = false
}
})
}
const handleEnterpriseCardRecall = async () => {
if (!enterpriseCardRecallFormRef.value) return
await enterpriseCardRecallFormRef.value.validate(async (valid) => {
if (!valid) return
const params = buildEnterpriseCardParams(enterpriseCardRecallForm)
if (!params) return
enterpriseCardRecallLoading.value = true
try {
const res = await EnterpriseService.recallCards(
enterpriseCardRecallForm.enterprise_id!,
params as any
)
if (res.code === 0) {
enterpriseCardRecallDialogVisible.value = false
await applyEnterpriseCardOperationSuccess('回收卡授权结果', res.data)
}
} catch (error) {
console.error('回收卡授权失败:', error)
} finally {
enterpriseCardRecallLoading.value = false
}
})
}
// 显示套餐系列绑定对话框
const showSeriesBindingDialog = async () => {
Object.assign(seriesBindingForm, createInitialSeriesBindingState(), {
@@ -2118,47 +2823,6 @@
})
}
// 清除卡套餐系列绑定
const handleClearCardSeries = async () => {
if (selectedCards.value.length === 0) {
ElMessage.warning('请先选择要清除关联的卡')
return
}
ElMessageBox.confirm(
`确定要清除所选 ${selectedCards.value.length} 张卡的套餐系列关联吗?`,
'确认清除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(async () => {
try {
const res = await CardService.batchSetCardSeriesBinding({
selection_type: CardSelectionType.LIST,
iccids: selectedCards.value.map((card) => card.iccid),
series_id: 0
})
if (res.code === 0) {
if (res.data.fail_count === 0) {
ElMessage.success('清除关联成功')
} else {
ElMessage.warning(`部分失败:${res.data.failed_items?.[0]?.reason || '未知原因'}`)
}
selectedCards.value = []
await getTableData()
}
} catch (error) {
console.error('清除关联失败:', error)
}
})
.catch(() => {
// 用户取消
})
}
// IoT卡操作处理函数
const handleCardOperation = (command: string, iccid: string, row?: any) => {
switch (command) {
@@ -2258,33 +2922,6 @@
// 用户取消
})
}
// 手动停用 IoT 卡(资产层面的停用)
const handleManualDeactivate = (iccid: string) => {
ElMessageBox.confirm(
`确定要手动停用卡片 ${iccid} 吗?此操作将在资产管理层面停用该卡。`,
'确认手动停用',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(async () => {
try {
const res = await AssetService.deactivateAsset(iccid)
if (res.code === 0) {
ElMessage.success('手动停用成功')
await getTableData()
}
} catch (error: any) {
console.error('手动停用失败:', error)
}
})
.catch(() => {
// 用户取消
})
}
</script>
<style lang="scss" scoped>