fetch(modify):完善ioT卡管理
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m37s

This commit is contained in:
sexygoat
2026-02-02 10:57:31 +08:00
parent e08c962c40
commit 78bd9fba85
2 changed files with 161 additions and 237 deletions

View File

@@ -198,23 +198,23 @@
<ElFormItem label="已选设备数">
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span>
</ElFormItem>
<ElFormItem label="套餐系列分配" prop="series_allocation_id">
<ElFormItem label="套餐系列" prop="series_id">
<ElSelect
v-model="seriesBindingForm.series_allocation_id"
placeholder="请选择或搜索套餐系列分配(支持系列名称搜索)"
v-model="seriesBindingForm.series_id"
placeholder="请选择或搜索套餐系列"
style="width: 100%"
filterable
remote
reserve-keyword
:remote-method="searchSeriesAllocations"
:remote-method="searchPackageSeries"
:loading="seriesLoading"
clearable
>
<ElOption label="清除关联" :value="0" />
<ElOption
v-for="series in seriesAllocationList"
v-for="series in packageSeriesList"
:key="series.id"
:label="`${series.series_name} (${series.shop_name})`"
:label="series.series_name"
:value="series.id"
:disabled="series.status !== 1"
/>
@@ -317,7 +317,7 @@
<!-- 绑定卡片列表弹窗 -->
<ElDialog v-model="deviceCardsDialogVisible" title="绑定的卡片" width="900px">
<div style="margin-bottom: 10px; text-align: right">
<ElButton type="primary" size="small" @click="handleBindCard">绑定新卡</ElButton>
<ElButton type="primary" @click="handleBindCard">绑定新卡</ElButton>
</div>
<div v-if="deviceCardsLoading" style="text-align: center; padding: 40px 0">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
@@ -402,8 +402,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { DeviceService, ShopService, CardService } from '@/api/modules'
import { ShopSeriesAllocationService } from '@/api/modules/shopSeriesAllocation'
import { DeviceService, ShopService, CardService, PackageSeriesService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElIcon, ElTreeSelect } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
@@ -419,7 +418,7 @@
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText } from '@/config/constants'
import type { ShopSeriesAllocationResponse } from '@/types/api'
import type { PackageSeriesResponse } from '@/types/api'
defineOptions({ name: 'DeviceList' })
@@ -442,12 +441,12 @@
const seriesBindingLoading = ref(false)
const seriesBindingFormRef = ref<FormInstance>()
const seriesLoading = ref(false)
const seriesAllocationList = ref<ShopSeriesAllocationResponse[]>([])
const packageSeriesList = ref<PackageSeriesResponse[]>([])
const seriesBindingForm = reactive({
series_allocation_id: undefined as number | undefined
series_id: undefined as number | undefined
})
const seriesBindingRules = reactive<FormRules>({
series_allocation_id: [{ required: true, message: '请选择套餐系列分配', trigger: 'change' }]
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }]
})
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
@@ -1144,14 +1143,14 @@
ElMessage.warning('请先选择要设置的设备')
return
}
seriesBindingForm.series_allocation_id = undefined
seriesBindingForm.series_id = undefined
seriesBindingResult.value = null
await loadSeriesAllocationList()
await loadPackageSeriesList()
seriesBindingDialogVisible.value = true
}
// 加载套餐系列分配列表(默认加载20条
const loadSeriesAllocationList = async (seriesName?: string) => {
// 加载套餐系列列表(支持名称搜索,默认20条
const loadPackageSeriesList = async (seriesName?: string) => {
seriesLoading.value = true
try {
const params: any = {
@@ -1162,21 +1161,21 @@
if (seriesName) {
params.series_name = seriesName
}
const res = await ShopSeriesAllocationService.getShopSeriesAllocations(params)
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0 && res.data.items) {
seriesAllocationList.value = res.data.items
packageSeriesList.value = res.data.items
}
} catch (error) {
console.error('获取套餐系列分配列表失败:', error)
ElMessage.error('获取套餐系列分配列表失败')
console.error('获取套餐系列列表失败:', error)
ElMessage.error('获取套餐系列列表失败')
} finally {
seriesLoading.value = false
}
}
// 搜索套餐系列分配(根据系列名称)
const searchSeriesAllocations = async (query: string) => {
await loadSeriesAllocationList(query || undefined)
// 搜索套餐系列
const searchPackageSeries = async (query: string) => {
await loadPackageSeriesList(query || undefined)
}
// 确认设置套餐系列绑定
@@ -1189,7 +1188,7 @@
try {
const data = {
device_ids: selectedDevices.value.map((d) => d.id),
series_allocation_id: seriesBindingForm.series_allocation_id!
series_allocation_id: seriesBindingForm.series_id! // 注意API参数名仍是series_allocation_id但前端使用series_id
}
const res = await DeviceService.batchSetDeviceSeriesBinding(data)
if (res.code === 0) {
@@ -1206,7 +1205,6 @@
}
} catch (error) {
console.error(error)
ElMessage.error('设置套餐系列绑定失败')
} finally {
seriesBindingLoading.value = false
}

View File

@@ -18,7 +18,6 @@
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showImportDialog">导入ICCID</ElButton>
<ElButton
type="success"
:disabled="selectedCards.length === 0"
@@ -33,6 +32,13 @@
>
批量回收
</ElButton>
<ElButton
type="primary"
:disabled="selectedCards.length === 0"
@click="showSeriesBindingDialog"
>
批量设置套餐系列
</ElButton>
<ElButton type="info" @contextmenu.prevent="showMoreMenu">更多操作</ElButton>
</template>
</ArtTableHeader>
@@ -57,63 +63,6 @@
</template>
</ArtTable>
<!-- 导入ICCID对话框 -->
<ElDialog
v-model="importDialogVisible"
title="导入ICCID"
width="500px"
@close="handleImportDialogClose"
>
<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%"
>
<ElOption label="中国移动" :value="1" />
<ElOption label="中国联通" :value="2" />
<ElOption label="中国电信" :value="3" />
</ElSelect>
</ElFormItem>
<ElFormItem label="批次号" prop="batch_no">
<ElInput v-model="importForm.batch_no" placeholder="请输入批次号(可选)" />
</ElFormItem>
<ElFormItem label="上传文件" prop="file">
<ElUpload
ref="uploadRef"
class="upload-demo"
:auto-upload="false"
:limit="1"
:on-change="handleFileChange"
:on-exceed="handleExceed"
:file-list="fileList"
accept=".csv"
>
<template #trigger>
<ElButton type="primary">选择文件</ElButton>
</template>
<template #tip>
<div class="el-upload__tip">
<div>只支持上传CSV文件且不超过10MB</div>
<div style="margin-top: 4px; color: var(--el-color-info)">
CSV格式ICCID,MSISDN两列逗号分隔每行一条记录
</div>
</div>
</template>
</ElUpload>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="importDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleImport" :loading="importLoading">
开始导入
</ElButton>
</div>
</template>
</ElDialog>
<!-- 批量分配对话框 -->
<ElDialog
v-model="allocateDialogVisible"
@@ -130,12 +79,21 @@
<ElFormItem label="目标店铺" prop="to_shop_id">
<ElSelect
v-model="allocateForm.to_shop_id"
placeholder="请选择目标店铺"
placeholder="请选择或搜索目标店铺"
filterable
remote
reserve-keyword
:remote-method="searchTargetShops"
:loading="targetShopLoading"
clearable
style="width: 100%"
>
<ElOption label="店铺A" :value="1" />
<ElOption label="店铺B" :value="2" />
<ElOption label="店铺C" :value="3" />
<ElOption
v-for="shop in targetShopList"
:key="shop.id"
:label="shop.shop_name"
:value="shop.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type">
@@ -224,12 +182,21 @@
<ElFormItem label="来源店铺" prop="from_shop_id">
<ElSelect
v-model="recallForm.from_shop_id"
placeholder="请选择来源店铺"
placeholder="请选择或搜索来源店铺"
filterable
remote
reserve-keyword
:remote-method="searchFromShops"
:loading="fromShopLoading"
clearable
style="width: 100%"
>
<ElOption label="店铺A" :value="1" />
<ElOption label="店铺B" :value="2" />
<ElOption label="店铺C" :value="3" />
<ElOption
v-for="shop in fromShopList"
:key="shop.id"
:label="shop.shop_name"
:value="shop.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type">
@@ -345,18 +312,23 @@
<ElFormItem label="已选择卡数">
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem label="套餐系列分配" prop="series_allocation_id">
<ElFormItem label="套餐系列" prop="series_id">
<ElSelect
v-model="seriesBindingForm.series_allocation_id"
placeholder="请选择套餐系列分配(选择清除关联将解除绑定)"
v-model="seriesBindingForm.series_id"
placeholder="请选择或搜索套餐系列"
style="width: 100%"
filterable
remote
reserve-keyword
:remote-method="searchPackageSeries"
:loading="seriesLoading"
clearable
>
<ElOption label="清除关联" :value="0" />
<ElOption
v-for="series in seriesAllocationList"
v-for="series in packageSeriesList"
:key="series.id"
:label="`${series.series_name} (${series.shop_name})`"
:label="series.series_name"
:value="series.id"
:disabled="series.status !== 1"
/>
@@ -506,11 +478,10 @@
<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, ElIcon } from 'element-plus'
import { CardService, ShopService, PackageSeriesService } from '@/api/modules'
import { ElMessage, ElTag, ElIcon } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import type { FormInstance, FormRules, UploadProps, UploadUserFile } 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'
@@ -524,14 +495,12 @@
AllocateStandaloneCardsResponse,
BatchSetCardSeriesBindingResponse
} from '@/types/api/card'
import type { ShopSeriesAllocationResponse } from '@/types/api'
import type { PackageSeriesResponse } from '@/types/api'
defineOptions({ name: 'StandaloneCardList' })
const router = useRouter()
const loading = ref(false)
const importDialogVisible = ref(false)
const importLoading = ref(false)
const allocateDialogVisible = ref(false)
const allocateLoading = ref(false)
const recallDialogVisible = ref(false)
@@ -539,11 +508,8 @@
const resultDialogVisible = ref(false)
const resultTitle = ref('')
const tableRef = ref()
const importFormRef = ref<FormInstance>()
const allocateFormRef = ref<FormInstance>()
const recallFormRef = ref<FormInstance>()
const uploadRef = ref()
const fileList = ref<UploadUserFile[]>([])
const selectedCards = ref<StandaloneIotCard[]>([])
const allocationResult = ref<AllocateStandaloneCardsResponse>({
allocation_no: '',
@@ -559,12 +525,12 @@
const seriesBindingResultDialogVisible = ref(false)
const seriesBindingFormRef = ref<FormInstance>()
const seriesLoading = ref(false)
const seriesAllocationList = ref<ShopSeriesAllocationResponse[]>([])
const packageSeriesList = ref<PackageSeriesResponse[]>([])
const seriesBindingForm = reactive({
series_allocation_id: undefined as number | undefined
series_id: undefined as number | undefined
})
const seriesBindingRules = reactive<FormRules>({
series_allocation_id: [{ required: true, message: '请选择套餐系列分配', trigger: 'change' }]
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }]
})
const seriesBindingResult = ref<BatchSetCardSeriesBindingResponse>({
success_count: 0,
@@ -580,6 +546,12 @@
// 更多操作右键菜单
const moreMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
// 店铺相关
const targetShopLoading = ref(false)
const fromShopLoading = ref(false)
const targetShopList = ref<any[]>([])
const fromShopList = ref<any[]>([])
// 搜索表单初始值
const initialSearchState = {
status: undefined,
@@ -592,19 +564,6 @@
// 搜索表单
const formFilters = reactive({ ...initialSearchState })
// 导入表单
const importForm = reactive({
carrier_id: undefined as number | undefined,
batch_no: '',
file: null as File | null
})
// 导入表单验证规则
const importRules = reactive<FormRules>({
carrier_id: [{ required: true, message: '请选择运营商', trigger: 'change' }],
file: [{ required: true, message: '请选择上传文件', trigger: 'change' }]
})
// 批量分配表单
const allocateForm = reactive<Partial<AllocateStandaloneCardsRequest>>({
selection_type: 'list',
@@ -1062,103 +1021,66 @@
getTableData()
}
// 显示导入对话框
const showImportDialog = () => {
importDialogVisible.value = true
importForm.carrier_id = undefined
importForm.batch_no = ''
importForm.file = null
fileList.value = []
if (importFormRef.value) {
importFormRef.value.resetFields()
}
}
// 文件变化处理
const handleFileChange: UploadProps['onChange'] = (file) => {
importForm.file = file.raw as File
if (importFormRef.value) {
importFormRef.value.clearValidate('file')
}
}
// 文件超出限制处理
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('最多只能上传1个文件')
}
// 关闭导入对话框
const handleImportDialogClose = () => {
if (uploadRef.value) {
uploadRef.value.clearFiles()
}
fileList.value = []
}
// 执行导入
const handleImport = async () => {
if (!importFormRef.value) return
await importFormRef.value.validate(async (valid) => {
if (valid) {
if (!importForm.file) {
ElMessage.warning('请选择上传文件')
return
}
importLoading.value = true
try {
// 确保 Content-Type 在获取 URL 和上传时完全一致
const contentType = importForm.file.type || 'text/csv'
// 1. 获取上传 URL
const uploadUrlRes = await StorageService.getUploadUrl({
file_name: importForm.file.name,
content_type: contentType,
purpose: 'iot_import'
})
if (uploadUrlRes.code !== 0) {
ElMessage.error('获取上传地址失败')
return
}
// 2. 上传文件到对象存储
await StorageService.uploadFile(
uploadUrlRes.data.upload_url,
importForm.file,
contentType
)
// 3. 调用导入接口
const importRes = await CardService.importIotCards({
carrier_id: importForm.carrier_id!,
file_key: uploadUrlRes.data.file_key,
batch_no: importForm.batch_no || undefined
})
if (importRes.code === 0) {
ElMessage.success(
importRes.data.message || '导入任务已创建,请到任务管理页面查看导入进度'
)
importDialogVisible.value = false
getTableData()
}
} catch (error) {
console.error(error)
ElMessage.error('导入失败,请重试')
} finally {
importLoading.value = false
}
}
})
}
// 表格选择变化
const handleSelectionChange = (selection: StandaloneIotCard[]) => {
selectedCards.value = selection
}
// 加载目标店铺列表
const loadTargetShops = async (shopName?: string) => {
targetShopLoading.value = true
try {
const params: any = {
page: 1,
page_size: 20
}
if (shopName) {
params.shop_name = shopName
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
targetShopList.value = res.data.items || []
}
} catch (error) {
console.error('获取目标店铺列表失败:', error)
} finally {
targetShopLoading.value = false
}
}
// 搜索目标店铺
const searchTargetShops = async (query: string) => {
await loadTargetShops(query || undefined)
}
// 加载来源店铺列表
const loadFromShops = async (shopName?: string) => {
fromShopLoading.value = true
try {
const params: any = {
page: 1,
page_size: 20
}
if (shopName) {
params.shop_name = shopName
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
fromShopList.value = res.data.items || []
}
} catch (error) {
console.error('获取来源店铺列表失败:', error)
} finally {
fromShopLoading.value = false
}
}
// 搜索来源店铺
const searchFromShops = async (query: string) => {
await loadFromShops(query || undefined)
}
// 显示批量分配对话框
const showAllocateDialog = () => {
if (selectedCards.value.length === 0) {
@@ -1177,6 +1099,8 @@
batch_no: '',
remark: ''
})
// 加载默认店铺列表
loadTargetShops()
if (allocateFormRef.value) {
allocateFormRef.value.resetFields()
}
@@ -1199,6 +1123,8 @@
batch_no: '',
remark: ''
})
// 加载默认店铺列表
loadFromShops()
if (recallFormRef.value) {
recallFormRef.value.resetFields()
}
@@ -1334,35 +1260,45 @@
return
}
// 加载套餐系列分配列表
await loadSeriesAllocationList()
// 加载套餐系列列表
await loadPackageSeriesList()
seriesBindingDialogVisible.value = true
seriesBindingForm.series_allocation_id = undefined
seriesBindingForm.series_id = undefined
if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields()
}
}
// 加载套餐系列分配列表
const loadSeriesAllocationList = async () => {
// 加载套餐系列列表支持名称搜索默认20条
const loadPackageSeriesList = async (seriesName?: string) => {
seriesLoading.value = true
try {
const res = await ShopSeriesAllocationService.getShopSeriesAllocations({
const params: any = {
page: 1,
page_size: 1000 // 获取所有可用的套餐系列分配
})
if (res.code === 0 && res.data.list) {
seriesAllocationList.value = res.data.list
page_size: 20,
status: 1 // 只获取启用的
}
if (seriesName) {
params.series_name = seriesName
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0 && res.data.items) {
packageSeriesList.value = res.data.items
}
} catch (error) {
console.error(error)
ElMessage.error('获取套餐系列分配列表失败')
console.error('获取套餐系列列表失败:', error)
ElMessage.error('获取套餐系列列表失败')
} finally {
seriesLoading.value = false
}
}
// 搜索套餐系列
const searchPackageSeries = async (query: string) => {
await loadPackageSeriesList(query || undefined)
}
// 关闭套餐系列绑定对话框
const handleSeriesBindingDialogClose = () => {
if (seriesBindingFormRef.value) {
@@ -1387,7 +1323,7 @@
try {
const res = await CardService.batchSetCardSeriesBinding({
iccids,
series_allocation_id: seriesBindingForm.series_allocation_id!
series_allocation_id: seriesBindingForm.series_id! // 注意API参数名仍是series_allocation_id但前端使用series_id
})
if (res.code === 0) {
@@ -1417,7 +1353,6 @@
}
} catch (error) {
console.error(error)
ElMessage.error('套餐系列绑定设置失败,请重试')
} finally {
seriesBindingLoading.value = false
}
@@ -1427,12 +1362,6 @@
// 更多操作菜单项配置
const moreMenuItems = computed((): MenuItemType[] => [
{
key: 'seriesBinding',
label: '批量设置套餐系列',
icon: '&#xe88e;',
disabled: selectedCards.value.length === 0
},
{
key: 'distribution',
label: '网卡分销',
@@ -1470,9 +1399,6 @@
// 处理更多操作菜单选择
const handleMoreMenuSelect = (item: MenuItemType) => {
switch (item.key) {
case 'seriesBinding':
showSeriesBindingDialog()
break
case 'distribution':
cardDistribution()
break