feat: 卡/设备是否有生效套餐
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m19s

This commit is contained in:
sexygoat
2026-05-19 15:14:39 +08:00
parent 14832f5c6f
commit 59167d2630
14 changed files with 3792 additions and 3963 deletions

View File

@@ -0,0 +1,43 @@
# Change: 为 IoT 卡管理和设备管理新增生效套餐筛选
## Why
当前后台的 IoT 卡管理页和设备管理页都缺少“是否有生效中的套餐”搜索条件,运营人员无法直接按资产当前是否存在生效中的套餐快速筛选目标记录。
本次需求要求两个列表统一新增该筛选能力,并通过查询参数 `has_active_package` 向后端传递筛选条件,避免用户依赖列表详情或手工比对来判断。
## What Changes
- 在 IoT 卡管理页搜索栏新增 `是否有生效中的套餐` 筛选项:
- 使用“是 / 否”布尔选项
-`GET /api/admin/iot-cards/standalone` 提交可选查询参数 `has_active_package`
- 未选择时不提交该参数
- 在设备管理页搜索栏新增 `是否有生效中的套餐` 筛选项:
- 使用“是 / 否”布尔选项
-`GET /api/admin/devices` 提交可选查询参数 `has_active_package`
- 未选择时不提交该参数
- 补齐前端类型契约与搜索状态管理:
- `StandaloneCardQueryParams` 支持 `has_active_package`
- `DeviceQueryParams` 支持 `has_active_package`
- 搜索、重置、翻页、刷新流程中正确保留或清空该条件
- 明确 `false` 是有效筛选值:
- 选择“否”时必须提交 `has_active_package=false`
- 前端不得因为通用空值清理或 `|| undefined` 之类的写法丢掉该参数
## Impact
- Affected specs:
- `iot-card-management`
- `device-management`
- Affected code:
- `src/types/api/card.ts`
- `src/types/api/device.ts`
- `src/api/modules/card.ts`
- `src/api/modules/device.ts`
- `src/views/asset-management/iot-card-management/index.vue`
- `src/views/asset-management/device-list/index.vue`
- Dependencies:
- `GET /api/admin/iot-cards/standalone`
- `GET /api/admin/devices`
- Breaking changes:
-

View File

@@ -0,0 +1,44 @@
## ADDED Requirements
### Requirement: Device Active Package Query Contract
The device management list integration SHALL support filtering records by whether the device currently has an active package via the query parameter `has_active_package`.
#### Scenario: Query devices with active package
- **GIVEN** 用户正在后台设备管理页查询列表
- **WHEN** 用户在 `是否有生效中的套餐` 中选择“是”并执行搜索
- **THEN** 系统 MUST 调用 `GET /api/admin/devices`
- **AND** 请求 MUST 提交 `has_active_package=true`
#### Scenario: Query devices without active package
- **GIVEN** 用户正在后台设备管理页查询列表
- **WHEN** 用户在 `是否有生效中的套餐` 中选择“否”并执行搜索
- **THEN** 系统 MUST 调用 `GET /api/admin/devices`
- **AND** 请求 MUST 提交 `has_active_package=false`
- **AND** 前端 MUST NOT 因为该值为 `false` 而省略此参数
#### Scenario: Omit active package filter when not selected
- **GIVEN** 用户未选择 `是否有生效中的套餐`
- **WHEN** 用户执行设备列表搜索、刷新或翻页
- **THEN** 列表请求 MUST NOT 携带 `has_active_package`
### Requirement: Device Active Package Search Filter
The device management page SHALL provide a search filter labeled `是否有生效中的套餐` and keep its selected state consistent across list interactions.
#### Scenario: Show yes and no options for active package filter
- **GIVEN** 用户打开后台设备管理页
- **WHEN** 页面渲染搜索栏
- **THEN** 页面 MUST 显示 `是否有生效中的套餐` 筛选项
- **AND** 该筛选项 MUST 提供“是”和“否”两个可选值
#### Scenario: Reset clears active package filter
- **GIVEN** 用户已经在 `是否有生效中的套餐` 中选择了“是”或“否”
- **WHEN** 用户重置搜索条件
- **THEN** 页面 MUST 清空该筛选项
- **AND** 后续列表请求 MUST NOT 再携带 `has_active_package`

View File

@@ -0,0 +1,44 @@
## ADDED Requirements
### Requirement: IoT Card Active Package Query Contract
The IoT card management list integration SHALL support filtering records by whether the card currently has an active package via the query parameter `has_active_package`.
#### Scenario: Query IoT cards with active package
- **GIVEN** 用户正在后台 IoT 卡管理页查询列表
- **WHEN** 用户在 `是否有生效中的套餐` 中选择“是”并执行搜索
- **THEN** 系统 MUST 调用 `GET /api/admin/iot-cards/standalone`
- **AND** 请求 MUST 提交 `has_active_package=true`
#### Scenario: Query IoT cards without active package
- **GIVEN** 用户正在后台 IoT 卡管理页查询列表
- **WHEN** 用户在 `是否有生效中的套餐` 中选择“否”并执行搜索
- **THEN** 系统 MUST 调用 `GET /api/admin/iot-cards/standalone`
- **AND** 请求 MUST 提交 `has_active_package=false`
- **AND** 前端 MUST NOT 因为该值为 `false` 而省略此参数
#### Scenario: Omit active package filter when not selected
- **GIVEN** 用户未选择 `是否有生效中的套餐`
- **WHEN** 用户执行 IoT 卡列表搜索、刷新或翻页
- **THEN** 列表请求 MUST NOT 携带 `has_active_package`
### Requirement: IoT Card Active Package Search Filter
The IoT card management page SHALL provide a search filter labeled `是否有生效中的套餐` and keep its selected state consistent across list interactions.
#### Scenario: Show yes and no options for active package filter
- **GIVEN** 用户打开后台 IoT 卡管理页
- **WHEN** 页面渲染搜索栏
- **THEN** 页面 MUST 显示 `是否有生效中的套餐` 筛选项
- **AND** 该筛选项 MUST 提供“是”和“否”两个可选值
#### Scenario: Reset clears active package filter
- **GIVEN** 用户已经在 `是否有生效中的套餐` 中选择了“是”或“否”
- **WHEN** 用户重置搜索条件
- **THEN** 页面 MUST 清空该筛选项
- **AND** 后续列表请求 MUST NOT 再携带 `has_active_package`

View File

@@ -0,0 +1,18 @@
## 1. Contract Alignment
- [ ] 1.1 为 `StandaloneCardQueryParams` 新增可选布尔参数 `has_active_package`
- [ ] 1.2 为 `DeviceQueryParams` 新增可选布尔参数 `has_active_package`
- [ ] 1.3 检查卡列表与设备列表请求参数组装逻辑,确保选择“否”时的 `has_active_package=false` 不会被误删
## 2. Search UI
- [ ] 2.1 在 IoT 卡管理页搜索栏新增 `是否有生效中的套餐` 筛选,并将选择结果提交为 `has_active_package`
- [ ] 2.2 在设备管理页搜索栏新增 `是否有生效中的套餐` 筛选,并将选择结果提交为 `has_active_package`
- [ ] 2.3 确保两个页面在搜索、重置、翻页和刷新时正确保留或清空该筛选条件
## 3. Verification
- [ ] 3.1 验证 IoT 卡管理页选择“是”时请求提交 `has_active_package=true`
- [ ] 3.2 验证 IoT 卡管理页选择“否”时请求提交 `has_active_package=false`,且不会被省略
- [ ] 3.3 验证设备管理页选择“是 / 否”时请求都能正确提交 `has_active_package`
- [ ] 3.4 验证两个页面重置搜索后,后续请求不再携带 `has_active_package`

View File

@@ -0,0 +1,31 @@
# Change: 更新批量号段分配数量提示与格式化展示
## Why
IoT 卡管理中的“批量分配”和“批量回收”已经支持按号段范围提交,但当前在输入起止 ICCID 后不会提示本次将覆盖多少张卡,运营人员仍需手动换算,容易误判批量范围。
同时,现有“批量设置套餐系列绑定”和“批量设置设备套餐系列绑定”虽然已经展示派生数量提示,但大范围操作时直接显示原始整数,可读性不足。需要将这些数量统一为带千分号的展示格式。
## What Changes
- 为 IoT 卡管理中的“批量分配”对话框新增号段范围派生数量提示,文案与现有系列绑定场景保持一致。
- 为 IoT 卡管理中的“批量回收”对话框新增号段范围派生数量提示,文案与现有系列绑定场景保持一致。
- 将以下批量 range 提示中的数量统一为千分号格式展示,例如 `1,000`
- IoT 卡“批量分配”
- IoT 卡“批量回收”
- IoT 卡“批量设置套餐系列绑定”
- 设备“批量设置设备套餐系列绑定”
- 不调整后端接口、请求参数或返回结构,仅补充前端展示与交互约束。
## Impact
- Affected specs:
- `iot-card-management`
- `iot-card-series-binding`
- `device-series-binding`
- Affected code:
- `src/views/asset-management/iot-card-management/index.vue`
- `src/views/asset-management/device-list/index.vue`
- Related changes:
- `update-asset-carrier-status-and-range-allocation-hints`
- `update-batch-series-binding-contract`

View File

@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: Device Batch Series Binding Range Allocation Hint Formatting
The device batch series binding dialog SHALL render its derived device-range allocation-count hint using decimal thousands separators.
The displayed count MUST continue to map to the same accepted numeric range as `virtual_no_end - virtual_no_start + 1`.
#### Scenario: Format device batch series binding range count with thousands separators
- **GIVEN** 用户打开“批量设置设备套餐系列绑定”对话框
- **AND** 用户选择 `selection_type="range"`
- **WHEN** 用户输入一个有效设备号范围,且派生数量为 `1000`
- **THEN** 对话框 MUST 显示 `共分配1,000台设备`
- **AND** 对话框 MUST NOT 显示未分组的 `共分配1000台设备`
- **AND** 当任一端点变化时,格式化后的数量 MUST 立即更新

View File

@@ -0,0 +1,53 @@
## ADDED Requirements
### Requirement: IoT Card Batch Allocation Range Allocation Hint
The IoT card batch allocation dialog SHALL provide a derived allocation-count hint for ICCID range selection.
The hint MUST appear immediately below the `iccid_start` and `iccid_end` inputs when `selection_type="range"`.
The count MUST be computed with exact integer arithmetic as `iccid_end - iccid_start + 1` so that long ICCID values do not lose precision.
When displayed, the count MUST use decimal thousands separators, for example `1,000`.
#### Scenario: Show allocation count for a valid ICCID range in batch allocation
- **GIVEN** 用户打开“批量分配”对话框
- **AND** 用户选择 `selection_type="range"`
- **WHEN** 用户输入有效的 `iccid_start``iccid_end`,且计算结果为正整数
- **THEN** 对话框 MUST 显示 `共分配xxx张卡`
- **AND** `xxx` MUST 等于 `iccid_end - iccid_start + 1`
- **AND** 当数量大于等于 `1000` 时,`xxx` MUST 使用千分号格式展示
- **AND** 当任一端点变化时,提示 MUST 立即更新
#### Scenario: Hide allocation count for incomplete or invalid ICCID ranges in batch allocation
- **GIVEN** 用户选择 `selection_type="range"`
- **WHEN** 任一起止 ICCID 为空、无法按整数计算,或计算结果小于 `1`
- **THEN** 对话框 MUST NOT 显示误导性的数量提示
### Requirement: IoT Card Batch Recall Range Allocation Hint
The IoT card batch recall dialog SHALL provide a derived allocation-count hint for ICCID range selection.
The hint MUST appear immediately below the `iccid_start` and `iccid_end` inputs when `selection_type="range"`.
The count MUST be computed with exact integer arithmetic as `iccid_end - iccid_start + 1` so that long ICCID values do not lose precision.
When displayed, the count MUST use decimal thousands separators, for example `1,000`.
#### Scenario: Show allocation count for a valid ICCID range in batch recall
- **GIVEN** 用户打开“批量回收”对话框
- **AND** 用户选择 `selection_type="range"`
- **WHEN** 用户输入有效的 `iccid_start``iccid_end`,且计算结果为正整数
- **THEN** 对话框 MUST 显示 `共分配xxx张卡`
- **AND** `xxx` MUST 等于 `iccid_end - iccid_start + 1`
- **AND** 当数量大于等于 `1000` 时,`xxx` MUST 使用千分号格式展示
- **AND** 当任一端点变化时,提示 MUST 立即更新
#### Scenario: Hide allocation count for incomplete or invalid ICCID ranges in batch recall
- **GIVEN** 用户选择 `selection_type="range"`
- **WHEN** 任一起止 ICCID 为空、无法按整数计算,或计算结果小于 `1`
- **THEN** 对话框 MUST NOT 显示误导性的数量提示

View File

@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: IoT Card Batch Series Binding Range Allocation Hint Formatting
The IoT card batch series binding dialog SHALL render its derived ICCID range allocation-count hint using decimal thousands separators.
The displayed count MUST continue to be derived from exact integer arithmetic as `iccid_end - iccid_start + 1`.
#### Scenario: Format batch series binding range count with thousands separators
- **GIVEN** 用户打开“批量设置套餐系列绑定”对话框
- **AND** 用户选择 `selection_type="range"`
- **WHEN** 用户输入一个有效 ICCID 范围,且派生数量为 `1000`
- **THEN** 对话框 MUST 显示 `共分配1,000张卡`
- **AND** 对话框 MUST NOT 显示未分组的 `共分配1000张卡`
- **AND** 当任一端点变化时,格式化后的数量 MUST 立即更新

View File

@@ -0,0 +1,10 @@
## 1. Specification
- [x] 1.1 为 IoT 卡管理“批量分配”补充号段范围派生数量提示 requirement并明确无效范围时不展示提示
- [x] 1.2 为 IoT 卡管理“批量回收”补充号段范围派生数量提示 requirement并明确无效范围时不展示提示
- [x] 1.3 为 IoT 卡“批量设置套餐系列绑定”和设备“批量设置设备套餐系列绑定”补充千分号格式化 requirement
- [x] 1.4 明确所有新增或既有的 `共分配` range 提示都使用统一的十进制千分位分组格式
## 2. Validation
- [x] 2.1 运行 `openspec validate update-batch-range-count-hints-formatting --strict`

View File

@@ -24,6 +24,8 @@ import type {
ImportIotCardResponse,
IotCardImportTask,
IotCardImportTaskDetail,
StandaloneCardQueryParams,
StandaloneIotCard,
BatchSetCardSeriesBindingRequest,
BatchSetCardSeriesBindingResponse
} from '@/types/api'
@@ -318,8 +320,10 @@ export class CardService extends BaseService {
* 获取单卡列表(未绑定设备)
* @param params 查询参数
*/
static getStandaloneIotCards(params?: any): Promise<PaginationResponse<any>> {
return this.getPage('/api/admin/iot-cards/standalone', params)
static getStandaloneIotCards(
params?: StandaloneCardQueryParams
): Promise<PaginationResponse<StandaloneIotCard>> {
return this.getPage<StandaloneIotCard>('/api/admin/iot-cards/standalone', params)
}
/**

View File

@@ -332,6 +332,9 @@ export enum StandaloneCardStatus {
// 单卡查询参数
export interface StandaloneCardQueryParams extends PaginationParams {
has_active_package?: boolean // has active package filter
carrier_name?: string // carrier name filter
is_standalone?: boolean // standalone filter
status?: StandaloneCardStatus // 状态
carrier_id?: number // 运营商ID
shop_id?: number // 分销商ID

View File

@@ -58,6 +58,7 @@ export interface Device {
// 设备查询参数
export interface DeviceQueryParams extends PaginationParams {
has_active_package?: boolean // has active package filter
virtual_no?: string // 虚拟号(模糊查询,原 device_no)
device_name?: string // 设备名称(模糊查询)
status?: DeviceStatus // 状态

View File

@@ -2,63 +2,32 @@
<ArtTableFullScreen>
<div class="device-list-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
label-width="100"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ArtSearchBar v-model:filter="searchForm" :items="searchFormItems" label-width="120" @reset="handleReset"
@search="handleSearch"></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<ArtTableHeader :columnList="columnOptions" v-model:columns="columnChecks" @refresh="handleRefresh">
<template #left>
<ElButton
type="primary"
@click="handleBatchAllocate"
:disabled="!selectedDevices.length"
v-permission="'device:batch_allocate'"
>
<ElButton type="primary" @click="handleBatchAllocate" :disabled="!selectedDevices.length"
v-permission="'device:batch_allocate'">
批量分配
</ElButton>
<ElButton
@click="handleBatchRecall"
:disabled="!selectedDevices.length"
v-permission="'device:batch_recall'"
>
<ElButton @click="handleBatchRecall" :disabled="!selectedDevices.length"
v-permission="'device:batch_recall'">
批量回收
</ElButton>
<ElButton
type="info"
@click="handleBatchSetSeries"
v-permission="'device:batch_set_series'"
>
<ElButton type="info" @click="handleBatchSetSeries" v-permission="'device:batch_set_series'">
批量设置套餐系列
</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="deviceList"
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:actions="getActions"
:actionsWidth="180"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@selection-change="handleSelectionChange"
>
<ArtTable ref="tableRef" row-key="id" :loading="loading" :data="deviceList" :currentPage="pagination.page"
:pageSize="pagination.pageSize" :total="pagination.total" :marginTop="10" :actions="getActions"
:actionsWidth="180" @size-change="handleSizeChange" @current-change="handleCurrentChange"
@selection-change="handleSelectionChange">
<template #default>
<ElTableColumn type="selection" width="55" />
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
@@ -67,43 +36,23 @@
<!-- 批量分配对话框 -->
<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="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span>
</ElFormItem>
<ElFormItem label="店铺" prop="target_shop_id">
<ElCascader
v-model="allocateForm.target_shop_id"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择目标店铺"
clearable
filterable
style="width: 100%"
/>
<ElCascader v-model="allocateForm.target_shop_id" :options="shopCascadeOptions" :props="shopCascadeProps"
placeholder="请选择目标店铺" clearable filterable style="width: 100%" />
</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>
<!-- 分配结果 -->
<div v-if="allocateResult" style="margin-top: 20px">
<ElAlert
:type="allocateResult.fail_count === 0 ? 'success' : 'warning'"
:closable="false"
style="margin-bottom: 10px"
>
<ElAlert :type="allocateResult.fail_count === 0 ? 'success' : 'warning'" :closable="false"
style="margin-bottom: 10px">
<template #title>
成功分配 {{ allocateResult.success_count }} 失败
{{ allocateResult.fail_count }}
@@ -111,11 +60,8 @@
</ElAlert>
<div v-if="allocateResult.failed_items && allocateResult.failed_items.length > 0">
<div style="margin-bottom: 10px; font-weight: bold">失败详情</div>
<div
v-for="item in allocateResult.failed_items"
:key="item.device_id"
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c"
>
<div v-for="item in allocateResult.failed_items" :key="item.device_id"
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c">
设备号: {{ item.virtual_no }} - {{ item.reason }}
</div>
</div>
@@ -126,12 +72,7 @@
<ElButton @click="handleCloseAllocateDialog">
{{ allocateResult ? '关闭' : '取消' }}
</ElButton>
<ElButton
v-if="!allocateResult"
type="primary"
@click="handleConfirmAllocate"
:loading="allocateLoading"
>
<ElButton v-if="!allocateResult" type="primary" @click="handleConfirmAllocate" :loading="allocateLoading">
确认分配
</ElButton>
</div>
@@ -145,33 +86,22 @@
<span style="font-weight: bold; color: #e6a23c">{{ selectedDevices.length }}</span>
</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>
<!-- 回收结果 -->
<div v-if="recallResult" style="margin-top: 20px">
<ElAlert
:type="recallResult.fail_count === 0 ? 'success' : 'warning'"
:closable="false"
style="margin-bottom: 10px"
>
<ElAlert :type="recallResult.fail_count === 0 ? 'success' : 'warning'" :closable="false"
style="margin-bottom: 10px">
<template #title>
成功回收 {{ recallResult.success_count }} 失败 {{ recallResult.fail_count }}
</template>
</ElAlert>
<div v-if="recallResult.failed_items && recallResult.failed_items.length > 0">
<div style="margin-bottom: 10px; font-weight: bold">失败详情</div>
<div
v-for="item in recallResult.failed_items"
:key="item.device_id"
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c"
>
<div v-for="item in recallResult.failed_items" :key="item.device_id"
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c">
设备号: {{ item.virtual_no }} - {{ item.reason }}
</div>
</div>
@@ -182,12 +112,7 @@
<ElButton @click="handleCloseRecallDialog">
{{ recallResult ? '关闭' : '取消' }}
</ElButton>
<ElButton
v-if="!recallResult"
type="primary"
@click="handleConfirmRecall"
:loading="recallLoading"
>
<ElButton v-if="!recallResult" type="primary" @click="handleConfirmRecall" :loading="recallLoading">
确认回收
</ElButton>
</div>
@@ -195,18 +120,9 @@
</ElDialog>
<!-- 批量设置套餐系列绑定对话框 -->
<ElDialog
v-model="seriesBindingDialogVisible"
title="批量设置设备套餐系列绑定"
width="600px"
@close="handleCloseSeriesBindingDialog"
>
<ElForm
ref="seriesBindingFormRef"
:model="seriesBindingForm"
:rules="seriesBindingRules"
label-width="120px"
>
<ElDialog v-model="seriesBindingDialogVisible" title="批量设置设备套餐系列绑定" width="600px"
@close="handleCloseSeriesBindingDialog">
<ElForm ref="seriesBindingFormRef" :model="seriesBindingForm" :rules="seriesBindingRules" label-width="120px">
<ElFormItem label="已选设备数">
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span>
</ElFormItem>
@@ -218,211 +134,90 @@
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.LIST"
label="设备列表"
>
<ElFormItem v-if="seriesBindingForm.selection_type === DeviceSelectionType.LIST" label="设备列表">
<div>已选择 {{ selectedDevices.length }} 台设备</div>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.RANGE"
label="起始设备号"
prop="virtual_no_start"
>
<ElInput
v-model="seriesBindingForm.virtual_no_start"
placeholder="请输入起始设备号"
/>
<ElFormItem v-if="seriesBindingForm.selection_type === DeviceSelectionType.RANGE" label="起始设备号"
prop="virtual_no_start">
<ElInput v-model="seriesBindingForm.virtual_no_start" placeholder="请输入起始设备号" />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.RANGE"
label="结束设备号"
prop="virtual_no_end"
>
<ElFormItem v-if="seriesBindingForm.selection_type === DeviceSelectionType.RANGE" label="结束设备号"
prop="virtual_no_end">
<ElInput v-model="seriesBindingForm.virtual_no_end" placeholder="请输入结束设备号" />
</ElFormItem>
<div
v-if="
<div v-if="
seriesBindingForm.selection_type === DeviceSelectionType.RANGE &&
seriesBindingRangeCountText
"
class="range-allocation-hint range-allocation-hint--device"
>
" class="range-allocation-hint range-allocation-hint--device">
共分配{{ seriesBindingRangeCountText }}台设备
</div>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="设备号"
>
<ElInput
v-model="seriesBindingForm.virtual_no"
placeholder="请输入设备号"
clearable
/>
<ElFormItem v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER" label="设备号">
<ElInput v-model="seriesBindingForm.virtual_no" placeholder="请输入设备号" clearable />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="设备名称"
>
<ElInput
v-model="seriesBindingForm.device_name"
placeholder="请输入设备名称"
clearable
/>
<ElFormItem v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER" label="设备名称">
<ElInput v-model="seriesBindingForm.device_name" placeholder="请输入设备名称" clearable />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="设备类型"
>
<ElInput
v-model="seriesBindingForm.device_type"
placeholder="请输入设备类型"
clearable
/>
<ElFormItem v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER" label="设备类型">
<ElInput v-model="seriesBindingForm.device_type" placeholder="请输入设备类型" clearable />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="制造商"
>
<ElInput
v-model="seriesBindingForm.manufacturer"
placeholder="请输入制造商"
clearable
/>
<ElFormItem v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER" label="制造商">
<ElInput v-model="seriesBindingForm.manufacturer" placeholder="请输入制造商" clearable />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="状态"
>
<ElSelect
v-model="seriesBindingForm.status"
placeholder="请选择状态"
clearable
style="width: 100%"
>
<ElFormItem v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER" label="状态">
<ElSelect v-model="seriesBindingForm.status" placeholder="请选择状态" clearable style="width: 100%">
<ElOption label="在库" :value="1" />
<ElOption label="已分销" :value="2" />
<ElOption label="已激活" :value="3" />
<ElOption label="已停用" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="批次号"
>
<ElSelect
v-model="seriesBindingForm.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"
/>
<ElFormItem v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER" label="批次号">
<ElSelect v-model="seriesBindingForm.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
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="店铺名称"
>
<ElCascader
v-model="seriesBindingForm.shop_id_path"
:options="seriesBindingShopCascadeOptions"
:props="seriesBindingShopCascadeProps"
placeholder="请选择店铺名称"
clearable
filterable
style="width: 100%"
/>
<ElFormItem v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER" label="店铺名称">
<ElCascader v-model="seriesBindingForm.shop_id_path" :options="seriesBindingShopCascadeOptions"
:props="seriesBindingShopCascadeProps" placeholder="请选择店铺名称" clearable filterable style="width: 100%" />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="当前套餐系列"
>
<ElSelect
v-model="seriesBindingForm.filter_series_id"
placeholder="请选择或搜索当前套餐系列"
clearable
filterable
remote
:remote-method="searchSeries"
style="width: 100%"
>
<ElOption
v-for="series in searchSeriesOptions"
:key="series.id"
:label="series.series_name"
:value="series.id"
/>
<ElFormItem v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER" label="当前套餐系列">
<ElSelect v-model="seriesBindingForm.filter_series_id" placeholder="请选择或搜索当前套餐系列" clearable filterable
remote :remote-method="searchSeries" style="width: 100%">
<ElOption v-for="series in searchSeriesOptions" :key="series.id" :label="series.series_name"
:value="series.id" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="创建设备时间"
>
<ElDatePicker
v-model="seriesBindingForm.created_at_range"
type="daterange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
<ElFormItem v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER" label="创建设备时间">
<ElDatePicker v-model="seriesBindingForm.created_at_range" type="daterange" range-separator="至"
start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD" style="width: 100%" />
</ElFormItem>
<ElFormItem label="目标套餐系列" prop="series_id">
<ElSelect
v-model="seriesBindingForm.series_id"
placeholder="请选择或搜索套餐系列"
style="width: 100%"
filterable
remote
reserve-keyword
:remote-method="searchPackageSeries"
:loading="seriesLoading"
clearable
>
<ElOption
v-for="series in packageSeriesList"
:key="series.id"
:label="series.series_name"
:value="series.id"
:disabled="series.status !== 1"
/>
<ElSelect v-model="seriesBindingForm.series_id" placeholder="请选择或搜索套餐系列" style="width: 100%" filterable
remote reserve-keyword :remote-method="searchPackageSeries" :loading="seriesLoading" clearable>
<ElOption v-for="series in packageSeriesList" :key="series.id" :label="series.series_name"
:value="series.id" :disabled="series.status !== 1" />
</ElSelect>
</ElFormItem>
</ElForm>
<!-- 设置结果 -->
<div v-if="seriesBindingResult" style="margin-top: 20px">
<ElAlert
:type="seriesBindingResult.fail_count === 0 ? 'success' : 'warning'"
:closable="false"
style="margin-bottom: 10px"
>
<ElAlert :type="seriesBindingResult.fail_count === 0 ? 'success' : 'warning'" :closable="false"
style="margin-bottom: 10px">
<template #title>
成功设置 {{ 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; font-size: 12px; color: #f56c6c"
>
<div v-for="item in seriesBindingResult.failed_items" :key="item.device_id"
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c">
设备号: {{ item.virtual_no }} - {{ item.reason }}
</div>
</div>
@@ -433,12 +228,8 @@
<ElButton @click="handleCloseSeriesBindingDialog">
{{ seriesBindingResult ? '关闭' : '取消' }}
</ElButton>
<ElButton
v-if="!seriesBindingResult"
type="primary"
@click="handleConfirmSeriesBinding"
:loading="seriesBindingLoading"
>
<ElButton v-if="!seriesBindingResult" type="primary" @click="handleConfirmSeriesBinding"
:loading="seriesBindingLoading">
确认设置
</ElButton>
</div>
@@ -448,7 +239,9 @@
<!-- 设备详情弹窗 -->
<ElDialog v-model="deviceDetailDialogVisible" title="设备详情" width="1000px">
<div v-if="deviceDetailLoading" style="padding: 40px 0; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<ElIcon class="is-loading" :size="40">
<Loading />
</ElIcon>
</div>
<div v-else-if="currentDeviceDetail">
<!-- 设备基本信息 -->
@@ -501,20 +294,11 @@
<template #header>
<div style="font-weight: bold">绑定新卡</div>
</template>
<ElForm
ref="bindCardFormRef"
:model="bindCardForm"
:rules="bindCardRules"
label-width="80"
>
<ElForm ref="bindCardFormRef" :model="bindCardForm" :rules="bindCardRules" label-width="80">
<ElRow :gutter="20">
<ElCol :span="6">
<ElFormItem label="搜索类型" prop="search_type">
<ElSelect
v-model="bindCardForm.search_type"
placeholder="请选择搜索类型"
style="width: 100%"
>
<ElSelect v-model="bindCardForm.search_type" placeholder="请选择搜索类型" style="width: 100%">
<ElOption label="ICCID" value="iccid" />
<ElOption label="运营商名称" value="carrier_name" />
</ElSelect>
@@ -522,60 +306,30 @@
</ElCol>
<ElCol :span="9">
<ElFormItem label="ICCID" prop="iot_card_id">
<ElSelect
v-model="bindCardForm.iot_card_id"
:placeholder="
bindCardForm.search_type === 'carrier_name'
<ElSelect v-model="bindCardForm.iot_card_id" :placeholder="bindCardForm.search_type === 'carrier_name'
? '请选择或搜索运营商名称'
: '请选择或搜索ICCID'
"
filterable
remote
reserve-keyword
:remote-method="searchIotCards"
:loading="iotCardSearchLoading"
clearable
style="width: 100%"
>
<ElOption
v-for="card in iotCardList"
:key="card.id"
" filterable remote reserve-keyword :remote-method="searchIotCards"
:loading="iotCardSearchLoading" clearable style="width: 100%">
<ElOption v-for="card in iotCardList" :key="card.id"
:label="`${card.iccid} ${card.carrier_name ? '(' + card.carrier_name + ')' : ''}`"
:value="card.id"
/>
<ElOption
v-if="iotCardList.length === 0 && !iotCardSearchLoading"
label="未找到相关数据"
value=""
disabled
/>
:value="card.id" />
<ElOption v-if="iotCardList.length === 0 && !iotCardSearchLoading" label="未找到相关数据" value=""
disabled />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="6">
<ElFormItem label="插槽位置" prop="slot_position">
<ElSelect
v-model="bindCardForm.slot_position"
placeholder="请选择插槽位置"
style="width: 100%"
>
<ElOption
v-for="i in currentDeviceDetail?.max_sim_slots || 4"
:key="i"
:label="`插槽 ${i}`"
:value="i"
/>
<ElSelect v-model="bindCardForm.slot_position" placeholder="请选择插槽位置" style="width: 100%">
<ElOption v-for="i in currentDeviceDetail?.max_sim_slots || 4" :key="i" :label="`插槽 ${i}`"
:value="i" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="3">
<ElButton
v-permission="'device:bind_card'"
type="primary"
@click="handleConfirmBindCard"
:loading="bindCardLoading"
style="width: 100%"
>
<ElButton v-permission="'device:bind_card'" type="primary" @click="handleConfirmBindCard"
:loading="bindCardLoading" style="width: 100%">
确认绑定
</ElButton>
</ElCol>
@@ -585,7 +339,9 @@
<!-- 已绑定卡片列表 -->
<div v-if="deviceCardsLoading" style="padding: 40px 0; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<ElIcon class="is-loading" :size="40">
<Loading />
</ElIcon>
</div>
<div v-else>
<ElTable :data="deviceCards" border style="width: 100%">
@@ -613,62 +369,36 @@
</ElTableColumn>
<ElTableColumn label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<ElButton
v-permission="'device:unbind_card'"
type="danger"
size="small"
link
@click="handleUnbindCard(row)"
>
<ElButton v-permission="'device:unbind_card'" type="danger" size="small" link
@click="handleUnbindCard(row)">
解绑
</ElButton>
</template>
</ElTableColumn>
</ElTable>
<div
v-if="deviceCards.length === 0"
style="padding: 20px; color: #909399; text-align: center"
>
<div v-if="deviceCards.length === 0" style="padding: 20px; color: #909399; text-align: center">
暂无设备绑定的卡
</div>
</div>
</ElDialog>
<!-- 切换SIM卡对话框 -->
<SwitchCardDialog
v-model="switchCardDialogVisible"
:device-info="currentOperatingImei"
:cards="deviceBindingCards"
:loading="loadingDeviceCards"
:confirm-loading="switchCardLoading"
@confirm="handleConfirmSwitchCard"
/>
<SwitchCardDialog v-model="switchCardDialogVisible" :device-info="currentOperatingImei"
:cards="deviceBindingCards" :loading="loadingDeviceCards" :confirm-loading="switchCardLoading"
@confirm="handleConfirmSwitchCard" />
<!-- 设置切卡模式对话框 -->
<SwitchModeDialog
v-model="switchModeDialogVisible"
:device-identifier="currentOperatingDeviceIdentifier"
:current-mode="currentDeviceSwitchMode"
:cards="switchModeCards"
:has-imei="currentOperatingDeviceHasImei"
:confirm-loading="switchModeLoading"
@confirm="handleConfirmSwitchMode"
/>
<SwitchModeDialog v-model="switchModeDialogVisible" :device-identifier="currentOperatingDeviceIdentifier"
:current-mode="currentDeviceSwitchMode" :cards="switchModeCards" :has-imei="currentOperatingDeviceHasImei"
:confirm-loading="switchModeLoading" @confirm="handleConfirmSwitchMode" />
<!-- 实名认证策略对话框 -->
<RealnamePolicyDialog
v-model="realnamePolicyDialogVisible"
:asset-identifier="currentRealnamePolicyAsset"
:current-policy="currentRealnamePolicy"
@confirm="handleConfirmRealnamePolicy"
/>
<RealnamePolicyDialog v-model="realnamePolicyDialogVisible" :asset-identifier="currentRealnamePolicyAsset"
:current-policy="currentRealnamePolicy" @confirm="handleConfirmRealnamePolicy" />
<!-- 操作审计日志弹窗 -->
<OperationLogsDialog
v-model="operationLogsDialogVisible"
:identifier="operationLogsIdentifier"
download-permission="device:download_log_file"
/>
<OperationLogsDialog v-model="operationLogsDialogVisible" :identifier="operationLogsIdentifier"
download-permission="device:download_log_file" />
</ElCard>
</div>
</ArtTableFullScreen>
@@ -842,6 +572,7 @@
return (end - start + 1n).toString()
}
const formatDigitGrouping = (value: string): string => value.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
const parseDeviceRangeEndpoint = (value: string): { prefix: string; number: bigint } | null => {
const normalizedValue = value.trim()
if (!normalizedValue) {
@@ -877,7 +608,8 @@
return ''
}
return getPositiveRangeCountText(start.number, end.number)
const countText = getPositiveRangeCountText(start.number, end.number)
return countText ? formatDigitGrouping(countText) : ''
}
const seriesBindingRangeCountText = computed(() =>
getDeviceRangeCountText(seriesBindingForm.virtual_no_start, seriesBindingForm.virtual_no_end)
@@ -978,6 +710,7 @@
manufacturer: '',
shop_id: undefined as number | undefined,
series_id: undefined as number | undefined,
has_active_package: undefined as boolean | undefined,
dateRange: [] as string[],
created_at_start: '',
created_at_end: ''
@@ -1149,6 +882,20 @@
]
// 分页
searchFormItems.splice(searchFormItems.length - 1, 0, {
label: '是否有生效套餐',
prop: 'has_active_package',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '是', value: true },
{ label: '否', value: false }
]
})
const pagination = reactive({
page: 1,
pageSize: 20,
@@ -1656,6 +1403,7 @@
manufacturer: searchForm.manufacturer || undefined,
shop_id: searchForm.shop_id || undefined,
series_id: searchForm.series_id || undefined,
has_active_package: searchForm.has_active_package ?? undefined,
created_at_start: searchForm.created_at_start || undefined,
created_at_end: searchForm.created_at_end || undefined
}

View File

@@ -2,64 +2,32 @@
<ArtTableFullScreen>
<div class="standalone-card-list-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
label-width="120"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ArtSearchBar v-model:filter="formFilters" :items="formItems" label-width="120" @reset="handleReset"
@search="handleSearch"></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<ArtTableHeader :columnList="columnOptions" v-model:columns="columnChecks" @refresh="handleRefresh">
<template #left>
<ElButton
type="success"
:disabled="selectedCards.length === 0"
@click="showAllocateDialog"
v-permission="'iot_card:batch_allocation'"
>
<ElButton type="success" :disabled="selectedCards.length === 0" @click="showAllocateDialog"
v-permission="'iot_card:batch_allocation'">
批量分配
</ElButton>
<ElButton
type="warning"
:disabled="selectedCards.length === 0"
@click="showRecallDialog"
v-permission="'iot_card:batch_recycle'"
>
<ElButton type="warning" :disabled="selectedCards.length === 0" @click="showRecallDialog"
v-permission="'iot_card:batch_recycle'">
批量回收
</ElButton>
<ElButton
type="primary"
@click="showSeriesBindingDialog"
v-permission="'iot_card:batch_setting'"
>
<ElButton type="primary" @click="showSeriesBindingDialog" v-permission="'iot_card:batch_setting'">
批量设置套餐系列
</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="cardList"
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:actions="getActions"
:actionsWidth="160"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@selection-change="handleSelectionChange"
>
<ArtTable ref="tableRef" row-key="id" :loading="loading" :data="cardList" :currentPage="pagination.page"
:pageSize="pagination.pageSize" :total="pagination.total" :marginTop="10" :actions="getActions"
:actionsWidth="160" @size-change="handleSizeChange" @current-change="handleCurrentChange"
@selection-change="handleSelectionChange">
<template #default>
<ElTableColumn type="selection" width="55" />
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
@@ -67,28 +35,11 @@
</ArtTable>
<!-- 批量分配对话框 -->
<ElDialog
v-model="allocateDialogVisible"
title="批量分配"
width="600px"
@close="handleAllocateDialogClose"
>
<ElForm
ref="allocateFormRef"
:model="allocateForm"
:rules="allocateRules"
label-width="120px"
>
<ElDialog v-model="allocateDialogVisible" title="批量分配" width="600px" @close="handleAllocateDialogClose">
<ElForm ref="allocateFormRef" :model="allocateForm" :rules="allocateRules" label-width="120px">
<ElFormItem label="店铺" prop="to_shop_id">
<ElCascader
v-model="allocateForm.to_shop_id"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择目标店铺"
clearable
filterable
style="width: 100%"
/>
<ElCascader v-model="allocateForm.to_shop_id" :options="shopCascadeOptions" :props="shopCascadeProps"
placeholder="请选择目标店铺" clearable filterable style="width: 100%" />
</ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="allocateForm.selection_type">
@@ -98,97 +49,50 @@
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<ElFormItem v-if="allocateForm.selection_type === CardSelectionType.LIST" label="ICCID列表">
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElFormItem v-if="allocateForm.selection_type === CardSelectionType.RANGE" label="起始ICCID"
prop="iccid_start">
<ElInput v-model="allocateForm.iccid_start" placeholder="请输入起始ICCID" />
</ElFormItem>
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElFormItem v-if="allocateForm.selection_type === CardSelectionType.RANGE" label="结束ICCID" prop="iccid_end">
<ElInput v-model="allocateForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<div v-if="
allocateForm.selection_type === CardSelectionType.RANGE && allocateRangeCountText
" class="range-allocation-hint range-allocation-hint--card-form">
共分配{{ allocateRangeCountText }}张卡
</div>
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.FILTER"
label="运营商"
>
<ElSelect
v-model="allocateForm.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"
/>
<ElFormItem v-if="allocateForm.selection_type === CardSelectionType.FILTER" label="运营商">
<ElSelect v-model="allocateForm.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
v-if="allocateForm.selection_type === CardSelectionType.FILTER"
label="卡状态"
>
<ElSelect
v-model="allocateForm.status"
placeholder="请选择状态"
clearable
style="width: 100%"
>
<ElFormItem v-if="allocateForm.selection_type === CardSelectionType.FILTER" label="卡状态">
<ElSelect v-model="allocateForm.status" placeholder="请选择状态" clearable style="width: 100%">
<ElOption label="在库" :value="1" />
<ElOption label="已分销" :value="2" />
<ElOption label="已激活" :value="3" />
<ElOption label="已停用" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="
<ElFormItem v-if="
allocateForm.selection_type === CardSelectionType.FILTER && userInfo.user_type !== 3
"
label="批次号"
>
<ElSelect
v-model="allocateForm.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"
/>
" label="批次号">
<ElSelect v-model="allocateForm.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="备注">
<ElInput
v-model="allocateForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
<ElInput v-model="allocateForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
</ElFormItem>
</ElForm>
<template #footer>
@@ -202,12 +106,7 @@
</ElDialog>
<!-- 批量回收对话框 -->
<ElDialog
v-model="recallDialogVisible"
title="批量回收"
width="600px"
@close="handleRecallDialogClose"
>
<ElDialog v-model="recallDialogVisible" title="批量回收" width="600px" @close="handleRecallDialogClose">
<ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules" label-width="120px">
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="recallForm.selection_type">
@@ -217,81 +116,40 @@
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<ElFormItem v-if="recallForm.selection_type === CardSelectionType.LIST" label="ICCID列表">
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElFormItem v-if="recallForm.selection_type === CardSelectionType.RANGE" label="起始ICCID" prop="iccid_start">
<ElInput v-model="recallForm.iccid_start" placeholder="请输入起始ICCID" />
</ElFormItem>
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElFormItem v-if="recallForm.selection_type === CardSelectionType.RANGE" label="结束ICCID" prop="iccid_end">
<ElInput v-model="recallForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<div v-if="recallForm.selection_type === CardSelectionType.RANGE && recallRangeCountText"
class="range-allocation-hint range-allocation-hint--card-form">
共分配{{ recallRangeCountText }}张卡
</div>
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.FILTER"
label="运营商"
>
<ElSelect
v-model="recallForm.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"
/>
<ElFormItem v-if="recallForm.selection_type === CardSelectionType.FILTER" label="运营商">
<ElSelect v-model="recallForm.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
v-if="
<ElFormItem v-if="
recallForm.selection_type === CardSelectionType.FILTER && userInfo.user_type !== 3
"
label="批次号"
>
<ElSelect
v-model="recallForm.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"
/>
" label="批次号">
<ElSelect v-model="recallForm.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="备注">
<ElInput
v-model="recallForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
<ElInput v-model="recallForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
</ElFormItem>
</ElForm>
<template #footer>
@@ -321,10 +179,8 @@
</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="200" />
@@ -340,18 +196,9 @@
</ElDialog>
<!-- 批量设置套餐系列绑定对话框 -->
<ElDialog
v-model="seriesBindingDialogVisible"
title="批量设置套餐系列绑定"
width="600px"
@close="handleSeriesBindingDialogClose"
>
<ElForm
ref="seriesBindingFormRef"
:model="seriesBindingForm"
:rules="seriesBindingRules"
label-width="100"
>
<ElDialog v-model="seriesBindingDialogVisible" title="批量设置套餐系列绑定" width="600px"
@close="handleSeriesBindingDialogClose">
<ElForm ref="seriesBindingFormRef" :model="seriesBindingForm" :rules="seriesBindingRules" label-width="100">
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="seriesBindingForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
@@ -359,116 +206,53 @@
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<ElFormItem v-if="seriesBindingForm.selection_type === CardSelectionType.LIST" label="ICCID列表">
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElFormItem v-if="seriesBindingForm.selection_type === CardSelectionType.RANGE" label="起始ICCID"
prop="iccid_start">
<ElInput v-model="seriesBindingForm.iccid_start" placeholder="请输入起始ICCID" />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElFormItem v-if="seriesBindingForm.selection_type === CardSelectionType.RANGE" label="结束ICCID"
prop="iccid_end">
<ElInput v-model="seriesBindingForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<div
v-if="
<div v-if="
seriesBindingForm.selection_type === CardSelectionType.RANGE &&
seriesBindingRangeCountText
"
class="range-allocation-hint range-allocation-hint--card"
>
" class="range-allocation-hint range-allocation-hint--card">
共分配{{ seriesBindingRangeCountText }}张卡
</div>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.FILTER"
label="运营商"
>
<ElSelect
v-model="seriesBindingForm.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"
/>
<ElFormItem v-if="seriesBindingForm.selection_type === CardSelectionType.FILTER" label="运营商">
<ElSelect v-model="seriesBindingForm.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
v-if="seriesBindingForm.selection_type === CardSelectionType.FILTER"
label="卡状态"
>
<ElSelect
v-model="seriesBindingForm.status"
placeholder="请选择状态"
clearable
style="width: 100%"
>
<ElFormItem v-if="seriesBindingForm.selection_type === CardSelectionType.FILTER" label="卡状态">
<ElSelect v-model="seriesBindingForm.status" placeholder="请选择状态" clearable style="width: 100%">
<ElOption label="在库" :value="1" />
<ElOption label="已分销" :value="2" />
<ElOption label="已激活" :value="3" />
<ElOption label="已停用" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="
<ElFormItem v-if="
seriesBindingForm.selection_type === CardSelectionType.FILTER &&
userInfo.user_type !== 3
"
label="批次号"
>
<ElSelect
v-model="seriesBindingForm.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"
/>
" label="批次号">
<ElSelect v-model="seriesBindingForm.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="套餐系列" prop="series_id">
<ElSelect
v-model="seriesBindingForm.series_id"
placeholder="请选择或搜索套餐系列"
style="width: 100%"
filterable
remote
reserve-keyword
:remote-method="searchPackageSeries"
:loading="seriesLoading"
clearable
>
<ElOption
v-for="series in packageSeriesList"
:key="series.id"
:label="series.series_name"
:value="series.id"
:disabled="series.status !== 1"
/>
<ElSelect v-model="seriesBindingForm.series_id" placeholder="请选择或搜索套餐系列" style="width: 100%" filterable
remote reserve-keyword :remote-method="searchPackageSeries" :loading="seriesLoading" clearable>
<ElOption v-for="series in packageSeriesList" :key="series.id" :label="series.series_name"
:value="series.id" :disabled="series.status !== 1" />
</ElSelect>
</ElFormItem>
</ElForm>
@@ -483,12 +267,8 @@
</ElDialog>
<!-- 套餐系列绑定结果对话框 -->
<ElDialog
v-model="seriesBindingResultDialogVisible"
title="设置结果"
width="700px"
@close="handleSeriesBindingResultDialogClose"
>
<ElDialog v-model="seriesBindingResultDialogVisible" title="设置结果" width="700px"
@close="handleSeriesBindingResultDialogClose">
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="成功数">
<ElTag type="success">{{ seriesBindingResult.success_count }}</ElTag>
@@ -498,10 +278,8 @@
</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="200" />
@@ -511,9 +289,7 @@
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="seriesBindingResultDialogVisible = false"
>确定</ElButton
>
<ElButton type="primary" @click="seriesBindingResultDialogVisible = false">确定</ElButton>
</div>
</template>
</ElDialog>
@@ -521,7 +297,9 @@
<!-- 卡详情对话框 -->
<ElDialog v-model="cardDetailDialogVisible" title="卡片详情" width="900px">
<div v-if="cardDetailLoading" style="padding: 40px; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<ElIcon class="is-loading" :size="40">
<Loading />
</ElIcon>
<div style="margin-top: 16px">加载中...</div>
</div>
@@ -568,9 +346,7 @@
{{ currentCardDetail.network_status === 1 ? '正常' : '停机' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="累计流量使用"
>{{ currentCardDetail.data_usage_mb }} MB</ElDescriptionsItem
>
<ElDescriptionsItem label="累计流量使用">{{ currentCardDetail.data_usage_mb }} MB</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{
formatDateTime(currentCardDetail.created_at)
}}</ElDescriptionsItem>
@@ -590,27 +366,17 @@
</ElDialog>
<!-- 实名认证策略对话框 -->
<RealnamePolicyDialog
v-model="realnamePolicyDialogVisible"
:asset-identifier="currentRealnamePolicyIccid"
:current-policy="currentRealnamePolicy"
@confirm="handleConfirmRealnamePolicy"
/>
<RealnamePolicyDialog v-model="realnamePolicyDialogVisible" :asset-identifier="currentRealnamePolicyIccid"
:current-policy="currentRealnamePolicy" @confirm="handleConfirmRealnamePolicy" />
<!-- 更新实名状态对话框 -->
<UpdateRealnameStatusDialog
v-model="updateRealnameStatusDialogVisible"
:asset-identifier="currentUpdateRealnameStatusIccid"
:current-realname-status="currentUpdateRealnameStatus"
@success="handleUpdateRealnameStatusSuccess"
/>
<UpdateRealnameStatusDialog v-model="updateRealnameStatusDialogVisible"
:asset-identifier="currentUpdateRealnameStatusIccid" :current-realname-status="currentUpdateRealnameStatus"
@success="handleUpdateRealnameStatusSuccess" />
<!-- 操作审计日志弹窗 -->
<OperationLogsDialog
v-model="operationLogsDialogVisible"
:identifier="operationLogsIdentifier"
download-permission="iot_card:download_log_file"
/>
<OperationLogsDialog v-model="operationLogsDialogVisible" :identifier="operationLogsIdentifier"
download-permission="iot_card:download_log_file" />
</ElCard>
</div>
</ArtTableFullScreen>
@@ -717,11 +483,23 @@
return (end - start + 1n).toString()
}
const seriesBindingRangeCountText = computed(() =>
getPositiveRangeCountText(
parseNumericStringToBigInt(seriesBindingForm.iccid_start),
parseNumericStringToBigInt(seriesBindingForm.iccid_end)
const formatDigitGrouping = (value: string): string => value.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
const getFormattedRangeCountText = (startValue?: string, endValue?: string): string => {
const countText = getPositiveRangeCountText(
parseNumericStringToBigInt(startValue ?? ''),
parseNumericStringToBigInt(endValue ?? '')
)
return countText ? formatDigitGrouping(countText) : ''
}
const allocateRangeCountText = computed(() =>
getFormattedRangeCountText(allocateForm.iccid_start, allocateForm.iccid_end)
)
const recallRangeCountText = computed(() =>
getFormattedRangeCountText(recallForm.iccid_start, recallForm.iccid_end)
)
const seriesBindingRangeCountText = computed(() =>
getFormattedRangeCountText(seriesBindingForm.iccid_start, seriesBindingForm.iccid_end)
)
const seriesBindingRules = reactive<FormRules>({
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
@@ -1013,6 +791,7 @@
device_virtual_no: string
shop_id: undefined | number
series_id: undefined | number
has_active_package: undefined | boolean
is_distributed: undefined | number
is_standalone: undefined | boolean
[key: string]: any
@@ -1025,6 +804,7 @@
device_virtual_no: '',
shop_id: undefined,
series_id: undefined,
has_active_package: undefined,
is_distributed: undefined,
is_standalone: undefined,
is_replaced: undefined,
@@ -1295,6 +1075,20 @@
]
// 搜索表单配置
baseFormItems.splice(8, 0, {
label: '是否有生效套餐',
prop: 'has_active_package',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '是', value: true },
{ label: '否', value: false }
]
})
const formItems: SearchFormItem[] = baseFormItems
// 列配置
@@ -2193,6 +1987,10 @@
margin: -6px 0 18px 100px;
}
.range-allocation-hint--card-form {
margin: -6px 0 18px 120px;
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}