Compare commits
5 Commits
feature/ga
...
iteration/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a62b8930d4 | ||
|
|
e41adb4505 | ||
|
|
2c8de04041 | ||
|
|
98a4be8719 | ||
|
|
f5730089a1 |
@@ -3,7 +3,6 @@ name: 构建并部署前端到测试环境
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- iteration/august
|
|
||||||
- dev
|
- dev
|
||||||
- test
|
- test
|
||||||
# 如果不再需要 main 分支触发构建,可以把下面这行删掉;
|
# 如果不再需要 main 分支触发构建,可以把下面这行删掉;
|
||||||
|
|||||||
23
openspec/changes/add-iot-card-recall-line-input/proposal.md
Normal file
23
openspec/changes/add-iot-card-recall-line-input/proposal.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Change: 批量回收支持一行一个 ICCID
|
||||||
|
|
||||||
|
用户已确认并完成实施;验证结果见 `tasks.md`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
IoT 卡管理的批量回收目前只能从表格勾选 ICCID、输入号段范围或使用筛选条件。已有卡号清单的用户需要直接粘贴每行一个 ICCID,并复用现有 ICCID 列表的回收接口。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 在“批量回收”弹窗的选卡方式中增加“一行一个ICCID”,选择后显示带标签和输入提示的多行文本框,沿用现有 Element Plus 表单样式。
|
||||||
|
- 按换行拆分输入,去除每行首尾空白并忽略空行;ICCID 始终按字符串处理,不作数字转换。不增加额外的卡号长度或格式限制。
|
||||||
|
- 提交时仍调用 `POST /api/admin/iot-cards/standalone/recall`,使用 `selection_type: "list"` 和 `iccids: string[]`,不向接口发送新的选卡方式或文本框字段。
|
||||||
|
- 没有有效输入时展示校验提示,不提交请求;切换方式后按当前方式构建参数,关闭再打开时清空手工输入。
|
||||||
|
- 取消批量回收入口必须勾选表格的限制。有勾选时仍默认“ICCID列表”,无勾选时默认“一行一个ICCID”;原有列表模式仍校验必须有勾选项。
|
||||||
|
- 保持号段范围、筛选条件、权限、备注、结果展示和刷新逻辑不变;不修改其他批量操作。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `iot-card-management`
|
||||||
|
- Affected code: `src/views/asset-management/iot-card-management/index.vue`
|
||||||
|
- Backend impact: 无;复用 `CardService.recallStandaloneCards` 及其现有列表请求结构。
|
||||||
|
- Related changes: `update-batch-range-count-hints-formatting`(保留其号段数量提示)。
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: IoT Card Batch Recall Line Input
|
||||||
|
|
||||||
|
The IoT card batch recall dialog SHALL offer “一行一个ICCID” as an additional frontend-only selection mode alongside the existing ICCID list, range and filter modes.
|
||||||
|
|
||||||
|
The new mode MUST show a labeled multiline input with a hint to enter one ICCID per line. It MUST split LF and CRLF line endings, trim each line, ignore empty lines and preserve each ICCID as a string without numeric conversion or additional length or format restrictions.
|
||||||
|
|
||||||
|
Submitting this mode MUST call the existing `POST /api/admin/iot-cards/standalone/recall` endpoint with `selection_type: "list"`, the entered values in `iccids`, and the existing optional remark. It MUST NOT include selected table rows, range fields, filter fields or frontend-only fields in that request.
|
||||||
|
|
||||||
|
#### Scenario: Submit a pasted ICCID list
|
||||||
|
|
||||||
|
- **GIVEN** 用户选择“一行一个ICCID”并输入多个 ICCID,行首尾存在空白且内容中包含空行
|
||||||
|
- **WHEN** 用户点击“确认回收”
|
||||||
|
- **THEN** 系统 MUST 去除首尾空白和空行,将剩余各行作为字符串放入 `iccids`
|
||||||
|
- **AND** 系统 MUST 使用原有列表模式的接口和 `selection_type: "list"` 提交
|
||||||
|
- **AND** 系统 MUST 沿用现有成功数、失败项及列表刷新流程
|
||||||
|
|
||||||
|
#### Scenario: Reject empty manual input
|
||||||
|
|
||||||
|
- **GIVEN** 用户选择“一行一个ICCID”
|
||||||
|
- **WHEN** 输入为空或仅包含空白和空行并尝试提交
|
||||||
|
- **THEN** 系统 MUST 显示请输入 ICCID 的校验提示
|
||||||
|
- **AND** 系统 MUST NOT 发送回收请求
|
||||||
|
|
||||||
|
#### Scenario: Keep each selection mode isolated
|
||||||
|
|
||||||
|
- **GIVEN** 用户在逐行输入与现有列表、号段或筛选方式之间切换
|
||||||
|
- **WHEN** 用户提交当前选择方式
|
||||||
|
- **THEN** 系统 MUST 仅根据当前方式构建请求参数
|
||||||
|
- **AND** 原有列表、号段和筛选方式 MUST 保持既有行为及号段数量提示
|
||||||
|
|
||||||
|
### Requirement: IoT Card Batch Recall Manual Input Entry
|
||||||
|
|
||||||
|
The batch recall action SHALL remain permission-controlled and be available without selected table rows so users can enter ICCIDs directly. When opening the dialog, it MUST default to the existing list mode if rows are selected and to line input mode otherwise. The existing list mode MUST still reject submission without selected cards. Manual input MUST be cleared when the dialog is reopened.
|
||||||
|
|
||||||
|
#### Scenario: Open batch recall without selected rows
|
||||||
|
|
||||||
|
- **GIVEN** 用户拥有 `iot_card:batch_recycle` 权限且未勾选表格行
|
||||||
|
- **WHEN** 用户点击“批量回收”
|
||||||
|
- **THEN** 系统 MUST 打开弹窗并默认选择“一行一个ICCID”
|
||||||
|
- **AND** 用户 MUST 能直接粘贴卡号,而无需先勾选无关卡片
|
||||||
|
|
||||||
|
#### Scenario: Preserve the selected-row workflow
|
||||||
|
|
||||||
|
- **GIVEN** 用户已在表格勾选卡片
|
||||||
|
- **WHEN** 用户打开“批量回收”弹窗
|
||||||
|
- **THEN** 系统 MUST 默认选择现有“ICCID列表”方式
|
||||||
|
- **AND** 用户按列表方式提交时 MUST 继续使用已勾选卡片的 ICCID
|
||||||
|
|
||||||
|
#### Scenario: Clear manual input when reopening
|
||||||
|
|
||||||
|
- **GIVEN** 用户在“一行一个ICCID”方式下输入过卡号并关闭弹窗
|
||||||
|
- **WHEN** 用户再次打开“批量回收”弹窗
|
||||||
|
- **THEN** 手工输入框 MUST 为空,不得沿用上一次回收的输入
|
||||||
31
openspec/changes/add-iot-card-recall-line-input/tasks.md
Normal file
31
openspec/changes/add-iot-card-recall-line-input/tasks.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## 1. Specification
|
||||||
|
|
||||||
|
- [x] 1.1 核对批量回收入口、选卡方式和现有列表模式接口。
|
||||||
|
- [x] 1.2 明确逐行输入、空值校验、接口复用和既有模式兼容场景。
|
||||||
|
- [x] 1.3 获得用户对提案的确认后开始实施(用户回复“执行”)。
|
||||||
|
|
||||||
|
## 2. Implementation
|
||||||
|
|
||||||
|
- [x] 2.1 新增仅用于前端的逐行输入选项和多行文本框,保留后端选卡方式枚举。
|
||||||
|
- [x] 2.2 实现按行解析、非空校验及 `selection_type: "list"` 的请求参数构建。
|
||||||
|
- [x] 2.3 调整无勾选时的入口和默认方式,并确保弹窗重开清空输入。
|
||||||
|
- [x] 2.4 保持既有列表、号段、筛选、权限及操作结果流程不变。
|
||||||
|
|
||||||
|
## 3. Validation
|
||||||
|
|
||||||
|
- [x] 3.1 运行 `openspec validate add-iot-card-recall-line-input --strict`(通过)。
|
||||||
|
- [x] 3.2 检查多行输入(LF / CRLF)、首尾空白、空行、全空输入和长 ICCID 字符串处理。
|
||||||
|
- [x] 3.3 检查模式切换、无勾选打开、有勾选默认列表、关闭重开及接口参数隔离。
|
||||||
|
- [x] 3.4 运行相关文件代码检查和项目类型检查,记录结果。
|
||||||
|
|
||||||
|
## 4. Verification results
|
||||||
|
|
||||||
|
- `node --test scripts/tests/iotCardRecallLineInput.test.mjs`:11 项通过;执行真实 Vue 组件 setup、校验规则和提交函数,并使用 mock 请求验证原有回收 API 的路径及参数,无真实回收操作。
|
||||||
|
- `node --import ./scripts/tests/register-esbuild.mjs --test scripts/tests/*.test.mjs`:项目全部 141 项测试通过(PowerShell 中先枚举测试文件再传入)。
|
||||||
|
- `pnpm exec eslint src/views/asset-management/iot-card-management/index.vue scripts/tests/iotCardRecallLineInput.test.mjs`:通过。
|
||||||
|
- `pnpm exec prettier --check src/views/asset-management/iot-card-management/index.vue scripts/tests/iotCardRecallLineInput.test.mjs`:通过。
|
||||||
|
- `node scripts/check-encoding.mjs src/views/asset-management/iot-card-management/index.vue scripts/tests/iotCardRecallLineInput.test.mjs`:通过。
|
||||||
|
- `pnpm exec vue-tsc --noEmit`:通过。
|
||||||
|
- `node scripts/tests/verifyBuild.mjs`:生产构建通过,输出到临时目录并保留生成声明文件;其他页面仍有 `searchForm` 常量赋值、循环依赖及大 chunk 警告,本次未修改这些页面。
|
||||||
|
- `openspec validate add-iot-card-recall-line-input --strict`、`git diff --check`:通过。
|
||||||
|
- 验证范围:本地自动化及构建检查,未执行真实后端回收或浏览器人工验收。工作区另有 `src/types/components.d.ts` 的生成变动,本次未回退或覆盖。
|
||||||
257
scripts/tests/iotCardRecallLineInput.test.mjs
Normal file
257
scripts/tests/iotCardRecallLineInput.test.mjs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import ts from 'typescript'
|
||||||
|
import * as vue from 'vue'
|
||||||
|
import { compileScript, parse } from '@vue/compiler-sfc'
|
||||||
|
|
||||||
|
function evaluate(source, imports = {}, globals = {}) {
|
||||||
|
const compiled = ts.transpileModule(source, {
|
||||||
|
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
||||||
|
}).outputText
|
||||||
|
const module = { exports: {} }
|
||||||
|
new Function('require', 'module', 'exports', ...Object.keys(globals), compiled)(
|
||||||
|
(name) => {
|
||||||
|
assert.ok(imports[name], `Unexpected dependency: ${name}`)
|
||||||
|
return imports[name]
|
||||||
|
},
|
||||||
|
module,
|
||||||
|
module.exports,
|
||||||
|
...Object.values(globals)
|
||||||
|
)
|
||||||
|
return module.exports
|
||||||
|
}
|
||||||
|
|
||||||
|
const read = (path) => readFileSync(path, 'utf8')
|
||||||
|
const cardTypes = evaluate(read('src/types/api/card.ts'))
|
||||||
|
const { descriptor } = parse(read('src/views/asset-management/iot-card-management/index.vue'))
|
||||||
|
const script = compileScript(descriptor, { id: 'iot-card-recall-lines' }).content
|
||||||
|
|
||||||
|
function harness() {
|
||||||
|
const writes = []
|
||||||
|
const warnings = []
|
||||||
|
const errors = []
|
||||||
|
let refreshes = 0
|
||||||
|
let response = {
|
||||||
|
code: 0,
|
||||||
|
data: { allocation_no: 'RECALL-1', total_count: 2, success_count: 1, fail_count: 1 }
|
||||||
|
}
|
||||||
|
class BaseService {
|
||||||
|
static async post(url, data) {
|
||||||
|
writes.push({ url, data: structuredClone(vue.toRaw(data)) })
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { CardService } = evaluate(read('src/api/modules/card.ts'), {
|
||||||
|
'../BaseService': { BaseService }
|
||||||
|
})
|
||||||
|
CardService.getStandaloneIotCards = async () => {
|
||||||
|
refreshes += 1
|
||||||
|
return { code: 0, data: { items: [], total: 0 } }
|
||||||
|
}
|
||||||
|
const runtimeVue = { ...vue, onMounted() {} }
|
||||||
|
const imports = {
|
||||||
|
vue: runtimeVue,
|
||||||
|
'vue-router': { useRouter: () => ({ push() {} }) },
|
||||||
|
'@/router/routesAlias': { RoutesAlias: {} },
|
||||||
|
'@/config/constants/audit': { AUDIT_PERMISSIONS: {} },
|
||||||
|
'@/utils/business/auditNavigation': { resolveAuditResourceTarget() {} },
|
||||||
|
'@/components/business/audit/investigationController': { openAuditInvestigation() {} },
|
||||||
|
'@/api/modules': {
|
||||||
|
CardService,
|
||||||
|
CarrierService: { getCarriers: async () => ({ code: 0, data: { items: [] } }) }
|
||||||
|
},
|
||||||
|
'element-plus': {
|
||||||
|
ElMessage: {
|
||||||
|
warning: (message) => warnings.push(message),
|
||||||
|
error: (message) => errors.push(message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'@element-plus/icons-vue': {},
|
||||||
|
'@/composables/useCheckedColumns': {
|
||||||
|
useCheckedColumns: () => ({ columnChecks: vue.ref([]), columns: vue.ref([]) })
|
||||||
|
},
|
||||||
|
'@/composables/useAuth': { useAuth: () => ({ hasAuth: () => true }) },
|
||||||
|
'@/store/modules/user': { useUserStore: () => ({}) },
|
||||||
|
pinia: { storeToRefs: () => ({ info: vue.ref({ user_type: 1 }) }) },
|
||||||
|
'@/utils/business/format': {},
|
||||||
|
'@/utils/business/expiryEstimate': {},
|
||||||
|
'@/utils/business/apiRateLimit': {},
|
||||||
|
'@/types/api/card': cardTypes
|
||||||
|
}
|
||||||
|
for (const name of [
|
||||||
|
'@/components/device/RealnamePolicyDialog.vue',
|
||||||
|
'@/components/business/BatchRealnamePolicyDialog.vue',
|
||||||
|
'@/components/business/UpdateRealnameStatusDialog.vue',
|
||||||
|
'@/components/business/ExportTaskCreateDialog.vue'
|
||||||
|
]) {
|
||||||
|
imports[name] = { default: {} }
|
||||||
|
}
|
||||||
|
const state = evaluate(script, imports, runtimeVue).default.setup({}, { expose() {} })
|
||||||
|
const validationErrors = []
|
||||||
|
state.recallFormRef.value = {
|
||||||
|
// Run the component's own validators for the fields mounted in the active mode.
|
||||||
|
async validate(callback) {
|
||||||
|
validationErrors.length = 0
|
||||||
|
const fields =
|
||||||
|
state.recallForm.selection_type === 'lines'
|
||||||
|
? ['iccids_text']
|
||||||
|
: state.recallForm.selection_type === 'range'
|
||||||
|
? ['iccid_start', 'iccid_end']
|
||||||
|
: []
|
||||||
|
for (const field of fields) {
|
||||||
|
for (const rule of state.recallRules[field]) {
|
||||||
|
rule.validator(rule, state.recallForm[field], (error) => {
|
||||||
|
if (error) validationErrors.push(error.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await callback(validationErrors.length === 0)
|
||||||
|
},
|
||||||
|
resetFields() {
|
||||||
|
// Emulate Element Plus restoring its initial selection when reopening.
|
||||||
|
state.recallForm.selection_type = 'list'
|
||||||
|
state.recallForm.iccids_text = ''
|
||||||
|
},
|
||||||
|
clearValidate() {
|
||||||
|
validationErrors.length = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
writes,
|
||||||
|
warnings,
|
||||||
|
errors,
|
||||||
|
validationErrors,
|
||||||
|
refreshes: () => refreshes,
|
||||||
|
setResponse(value) {
|
||||||
|
response = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('unselected entry defaults to line input; reopening resets input without losing default', () => {
|
||||||
|
const { state } = harness()
|
||||||
|
state.showRecallDialog()
|
||||||
|
assert.equal(state.recallDialogVisible.value, true)
|
||||||
|
assert.equal(state.recallForm.selection_type, 'lines')
|
||||||
|
state.recallForm.iccids_text = 'OLD-CARD'
|
||||||
|
state.handleRecallDialogClose()
|
||||||
|
assert.equal(state.recallForm.iccids_text, '')
|
||||||
|
state.showRecallDialog()
|
||||||
|
assert.equal(state.recallForm.selection_type, 'lines')
|
||||||
|
assert.equal(state.recallForm.iccids_text, '')
|
||||||
|
state.selectedCards.value = [{ iccid: 'SELECTED-CARD' }]
|
||||||
|
state.showRecallDialog()
|
||||||
|
assert.equal(state.recallForm.selection_type, 'list')
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const ending of ['\n', '\r\n']) {
|
||||||
|
test(`line mode trims and skips blank lines, preserves strings, and reuses list API (${JSON.stringify(ending)})`, async () => {
|
||||||
|
const { state, writes, refreshes } = harness()
|
||||||
|
state.selectedCards.value = [{ iccid: 'MUST-NOT-BE-SENT' }]
|
||||||
|
state.showRecallDialog()
|
||||||
|
Object.assign(state.recallForm, {
|
||||||
|
selection_type: 'lines',
|
||||||
|
iccids_text: [' 089860012345678901234 ', '', '\t', '89860012345678901235F'].join(ending),
|
||||||
|
iccid_start: 'OLD-START',
|
||||||
|
iccid_end: 'OLD-END',
|
||||||
|
carrier_id: 7,
|
||||||
|
batch_no: 'OLD-BATCH',
|
||||||
|
remark: '回收测试'
|
||||||
|
})
|
||||||
|
await state.handleRecall()
|
||||||
|
assert.deepEqual(writes, [
|
||||||
|
{
|
||||||
|
url: '/api/admin/iot-cards/standalone/recall',
|
||||||
|
data: {
|
||||||
|
selection_type: 'list',
|
||||||
|
iccids: ['089860012345678901234', '89860012345678901235F'],
|
||||||
|
remark: '回收测试'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
assert.equal(state.recallDialogVisible.value, false)
|
||||||
|
assert.equal(state.resultDialogVisible.value, true)
|
||||||
|
assert.equal(state.resultTitle.value, '批量回收结果')
|
||||||
|
assert.equal(state.allocationResult.value.success_count, 1)
|
||||||
|
assert.equal(state.allocationResult.value.fail_count, 1)
|
||||||
|
assert.deepEqual(state.selectedCards.value, [])
|
||||||
|
assert.equal(state.recallLoading.value, false)
|
||||||
|
assert.equal(refreshes(), 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const text of ['', ' \r\n \t\n']) {
|
||||||
|
test(`empty input is rejected without a request (${JSON.stringify(text)})`, async () => {
|
||||||
|
const { state, writes, validationErrors } = harness()
|
||||||
|
state.showRecallDialog()
|
||||||
|
state.recallForm.iccids_text = text
|
||||||
|
await state.handleRecall()
|
||||||
|
assert.deepEqual(writes, [])
|
||||||
|
assert.deepEqual(validationErrors, ['请输入ICCID,一行一个'])
|
||||||
|
assert.equal(state.recallDialogVisible.value, true)
|
||||||
|
assert.equal(state.recallLoading.value, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [mode, expected] of [
|
||||||
|
['list', { iccids: ['SELECTED-CARD'] }],
|
||||||
|
['range', { iccid_start: '89860000000000000000', iccid_end: '89860000000000000999' }],
|
||||||
|
['filter', { carrier_id: 7, batch_no: 'BATCH-1' }]
|
||||||
|
]) {
|
||||||
|
test(`switching from line input to ${mode} preserves its existing parameters`, async () => {
|
||||||
|
const { state, writes } = harness()
|
||||||
|
state.selectedCards.value = [{ iccid: 'SELECTED-CARD' }]
|
||||||
|
state.showRecallDialog()
|
||||||
|
Object.assign(state.recallForm, {
|
||||||
|
selection_type: 'lines',
|
||||||
|
iccids_text: 'MANUAL-CARD',
|
||||||
|
iccid_start: '89860000000000000000',
|
||||||
|
iccid_end: '89860000000000000999',
|
||||||
|
carrier_id: 7,
|
||||||
|
batch_no: 'BATCH-1'
|
||||||
|
})
|
||||||
|
state.recallForm.selection_type = mode
|
||||||
|
assert.equal(state.recallRangeCountText.value, '1,000')
|
||||||
|
await state.handleRecall()
|
||||||
|
assert.deepEqual(writes[0].data, { selection_type: mode, remark: '', ...expected })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test('list mode still rejects an empty table selection', async () => {
|
||||||
|
const { state, writes, warnings } = harness()
|
||||||
|
state.showRecallDialog()
|
||||||
|
state.recallForm.selection_type = 'list'
|
||||||
|
await state.handleRecall()
|
||||||
|
assert.deepEqual(writes, [])
|
||||||
|
assert.deepEqual(warnings, ['请先选择要回收的卡'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('failed requests retain manual input for retry and do not refresh the table', async () => {
|
||||||
|
const { state, errors, setResponse, refreshes } = harness()
|
||||||
|
state.showRecallDialog()
|
||||||
|
state.recallForm.iccids_text = 'RETRY-CARD'
|
||||||
|
setResponse({ code: 1, msg: '回收失败' })
|
||||||
|
await state.handleRecall()
|
||||||
|
assert.deepEqual(errors, ['回收失败'])
|
||||||
|
assert.equal(state.recallForm.iccids_text, 'RETRY-CARD')
|
||||||
|
assert.equal(state.recallDialogVisible.value, true)
|
||||||
|
assert.equal(state.recallLoading.value, false)
|
||||||
|
assert.equal(refreshes(), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('template keeps recall permission, enables its entry, and binds the labeled textarea', () => {
|
||||||
|
const source = descriptor.template.content
|
||||||
|
const entry = source.match(/<ElButton\b[^>]*@click="showRecallDialog"[^>]*>/)[0]
|
||||||
|
assert.match(entry, /v-permission="'iot_card:batch_recycle'"/)
|
||||||
|
assert.doesNotMatch(entry, /:disabled/)
|
||||||
|
const dialog = source.match(/<ElDialog\s+v-model="recallDialogVisible"[\s\S]*?<\/ElDialog>/)[0]
|
||||||
|
assert.match(dialog, /<ElRadio label="lines">一行一个ICCID<\/ElRadio>/)
|
||||||
|
assert.match(
|
||||||
|
dialog,
|
||||||
|
/v-if="recallForm\.selection_type === 'lines'"\s+label="ICCID"\s+prop="iccids_text"/
|
||||||
|
)
|
||||||
|
assert.match(dialog, /v-model="recallForm\.iccids_text"\s+type="textarea"/)
|
||||||
|
assert.match(dialog, /placeholder="请输入ICCID,一行一个"/)
|
||||||
|
})
|
||||||
@@ -28,7 +28,6 @@
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
type="warning"
|
type="warning"
|
||||||
:disabled="selectedCards.length === 0"
|
|
||||||
@click="showRecallDialog"
|
@click="showRecallDialog"
|
||||||
v-permission="'iot_card:batch_recycle'"
|
v-permission="'iot_card:batch_recycle'"
|
||||||
>
|
>
|
||||||
@@ -254,6 +253,7 @@
|
|||||||
@change="handleCardBatchSelectionTypeChange"
|
@change="handleCardBatchSelectionTypeChange"
|
||||||
>
|
>
|
||||||
<ElRadio label="list">ICCID列表</ElRadio>
|
<ElRadio label="list">ICCID列表</ElRadio>
|
||||||
|
<ElRadio label="lines">一行一个ICCID</ElRadio>
|
||||||
<ElRadio label="range">号段范围</ElRadio>
|
<ElRadio label="range">号段范围</ElRadio>
|
||||||
<ElRadio label="filter">筛选条件</ElRadio>
|
<ElRadio label="filter">筛选条件</ElRadio>
|
||||||
</ElRadioGroup>
|
</ElRadioGroup>
|
||||||
@@ -266,6 +266,19 @@
|
|||||||
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem
|
||||||
|
v-if="recallForm.selection_type === 'lines'"
|
||||||
|
label="ICCID"
|
||||||
|
prop="iccids_text"
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
v-model="recallForm.iccids_text"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
placeholder="请输入ICCID,一行一个"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
v-if="recallForm.selection_type === CardSelectionType.RANGE"
|
v-if="recallForm.selection_type === CardSelectionType.RANGE"
|
||||||
label="起始ICCID"
|
label="起始ICCID"
|
||||||
@@ -1654,19 +1667,43 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 批量回收表单
|
// 批量回收表单
|
||||||
const recallForm = reactive<Partial<RecallStandaloneCardsRequest>>({
|
const recallForm = reactive<
|
||||||
|
Omit<Partial<RecallStandaloneCardsRequest>, 'selection_type'> & {
|
||||||
|
selection_type: CardSelectionType | 'lines'
|
||||||
|
iccids_text: string
|
||||||
|
}
|
||||||
|
>({
|
||||||
selection_type: CardSelectionType.LIST,
|
selection_type: CardSelectionType.LIST,
|
||||||
iccids: [],
|
iccids: [],
|
||||||
|
iccids_text: '',
|
||||||
iccid_start: '',
|
iccid_start: '',
|
||||||
iccid_end: '',
|
iccid_end: '',
|
||||||
carrier_id: undefined,
|
carrier_id: undefined,
|
||||||
batch_no: '',
|
batch_no: '',
|
||||||
remark: ''
|
remark: ''
|
||||||
})
|
})
|
||||||
|
const recallInputIccids = computed(() =>
|
||||||
|
recallForm.iccids_text
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((iccid) => iccid.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
)
|
||||||
|
|
||||||
// 批量回收表单验证规则
|
// 批量回收表单验证规则
|
||||||
const recallRules = reactive<FormRules>({
|
const recallRules = reactive<FormRules>({
|
||||||
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
|
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
|
||||||
|
iccids_text: [
|
||||||
|
{
|
||||||
|
validator: (_rule, _value, callback) => {
|
||||||
|
if (recallForm.selection_type === 'lines' && recallInputIccids.value.length === 0) {
|
||||||
|
callback(new Error('请输入ICCID,一行一个'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
iccid_start: [
|
iccid_start: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
@@ -2068,7 +2105,7 @@
|
|||||||
prop: 'iccid',
|
prop: 'iccid',
|
||||||
label: 'ICCID',
|
label: 'ICCID',
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
fixed:true,
|
fixed: true,
|
||||||
formatter: (row: StandaloneIotCard) => {
|
formatter: (row: StandaloneIotCard) => {
|
||||||
return h(
|
return h(
|
||||||
'span',
|
'span',
|
||||||
@@ -2523,23 +2560,19 @@
|
|||||||
|
|
||||||
// 显示批量回收对话框
|
// 显示批量回收对话框
|
||||||
const showRecallDialog = () => {
|
const showRecallDialog = () => {
|
||||||
if (selectedCards.value.length === 0) {
|
recallFormRef.value?.resetFields()
|
||||||
ElMessage.warning('请先选择要回收的卡')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
recallDialogVisible.value = true
|
|
||||||
Object.assign(recallForm, {
|
Object.assign(recallForm, {
|
||||||
selection_type: CardSelectionType.LIST,
|
selection_type: selectedCards.value.length > 0 ? CardSelectionType.LIST : 'lines',
|
||||||
iccids: selectedCards.value.map((card) => card.iccid),
|
iccids: selectedCards.value.map((card) => card.iccid),
|
||||||
|
iccids_text: '',
|
||||||
iccid_start: '',
|
iccid_start: '',
|
||||||
iccid_end: '',
|
iccid_end: '',
|
||||||
carrier_id: undefined,
|
carrier_id: undefined,
|
||||||
batch_no: '',
|
batch_no: '',
|
||||||
remark: ''
|
remark: ''
|
||||||
})
|
})
|
||||||
if (recallFormRef.value) {
|
recallFormRef.value?.clearValidate()
|
||||||
recallFormRef.value.resetFields()
|
recallDialogVisible.value = true
|
||||||
}
|
|
||||||
loadAllocateCarrierOptions()
|
loadAllocateCarrierOptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2555,6 +2588,7 @@
|
|||||||
if (recallFormRef.value) {
|
if (recallFormRef.value) {
|
||||||
recallFormRef.value.resetFields()
|
recallFormRef.value.resetFields()
|
||||||
}
|
}
|
||||||
|
recallForm.iccids_text = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行批量分配
|
// 执行批量分配
|
||||||
@@ -2621,11 +2655,16 @@
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
// 根据选卡方式构建请求参数
|
// 根据选卡方式构建请求参数
|
||||||
const params: Partial<RecallStandaloneCardsRequest> = {
|
const params: Partial<RecallStandaloneCardsRequest> = {
|
||||||
selection_type: recallForm.selection_type!,
|
selection_type:
|
||||||
|
recallForm.selection_type === 'lines'
|
||||||
|
? CardSelectionType.LIST
|
||||||
|
: recallForm.selection_type,
|
||||||
remark: recallForm.remark
|
remark: recallForm.remark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recallForm.selection_type === CardSelectionType.LIST) {
|
if (recallForm.selection_type === 'lines') {
|
||||||
|
params.iccids = recallInputIccids.value
|
||||||
|
} else if (recallForm.selection_type === CardSelectionType.LIST) {
|
||||||
params.iccids = selectedCards.value.map((card) => card.iccid)
|
params.iccids = selectedCards.value.map((card) => card.iccid)
|
||||||
if (params.iccids.length === 0) {
|
if (params.iccids.length === 0) {
|
||||||
ElMessage.warning('请先选择要回收的卡')
|
ElMessage.warning('请先选择要回收的卡')
|
||||||
@@ -2718,10 +2757,7 @@
|
|||||||
|
|
||||||
const showEnterpriseCardRecallDialog = async () => {
|
const showEnterpriseCardRecallDialog = async () => {
|
||||||
resetEnterpriseCardRecallForm()
|
resetEnterpriseCardRecallForm()
|
||||||
await Promise.all([
|
await Promise.all([handleSearchEnterprise(), loadAllocateCarrierOptions()])
|
||||||
handleSearchEnterprise(),
|
|
||||||
loadAllocateCarrierOptions()
|
|
||||||
])
|
|
||||||
enterpriseCardRecallDialogVisible.value = true
|
enterpriseCardRecallDialogVisible.value = true
|
||||||
enterpriseCardRecallFormRef.value?.clearValidate()
|
enterpriseCardRecallFormRef.value?.clearValidate()
|
||||||
}
|
}
|
||||||
@@ -2841,10 +2877,7 @@
|
|||||||
selection_type:
|
selection_type:
|
||||||
selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER
|
selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER
|
||||||
})
|
})
|
||||||
await Promise.all([
|
await Promise.all([loadPackageSeriesList(), loadAllocateCarrierOptions()])
|
||||||
loadPackageSeriesList(),
|
|
||||||
loadAllocateCarrierOptions()
|
|
||||||
])
|
|
||||||
seriesBindingDialogVisible.value = true
|
seriesBindingDialogVisible.value = true
|
||||||
seriesBindingFormRef.value?.clearValidate()
|
seriesBindingFormRef.value?.clearValidate()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user