From 22e796ba238b8d053b8f6cd2aa65af342d2e8c85 Mon Sep 17 00:00:00 2001 From: luo Date: Tue, 20 Oct 2026 17:35:12 +0800 Subject: [PATCH] fix:some --- scripts/tests/carrierRealnameControl.test.mjs | 140 +++++++++++++++++- src/types/api/carrier.ts | 1 + src/views/system/carrier-management/index.vue | 41 ++++- 3 files changed, 173 insertions(+), 9 deletions(-) diff --git a/scripts/tests/carrierRealnameControl.test.mjs b/scripts/tests/carrierRealnameControl.test.mjs index 7f3c296..315589f 100644 --- a/scripts/tests/carrierRealnameControl.test.mjs +++ b/scripts/tests/carrierRealnameControl.test.mjs @@ -38,6 +38,11 @@ const components = Object.fromEntries( }) ) +test('carrier form grid top-aligns items so helper text cannot offset adjacent controls', () => { + const style = components.index.descriptor.styles.map((block) => block.content).join('\n') + assert.match(style, /\.carrier-form-grid\s*\{[^}]*align-items:\s*start\s*;/) +}) + const carrier = (enabled) => ({ id: 7, carrier_code: 'carrier-test', @@ -51,6 +56,7 @@ const carrier = (enabled) => ({ function harness(userType, responseCarrier = carrier(1)) { const writes = [] + const confirmations = [] const user = vue.reactive({ info: { user_type: userType }, permissions: [], buttons: [] }) const permission = evaluate(read('src/composables/usePermission.ts'), { vue, @@ -70,7 +76,12 @@ function harness(userType, responseCarrier = carrier(1)) { '@/api/modules': { CarrierService: service }, 'element-plus': { ElMessage: { success() {}, warning() {} }, - ElMessageBox: {}, + ElMessageBox: { + confirm: async (...args) => { + confirmations.push(args) + return 'confirm' + } + }, ElTag: {}, ElSwitch: {}, ElButton: {} @@ -91,7 +102,14 @@ function harness(userType, responseCarrier = carrier(1)) { } const setup = (name) => evaluate(components[name].script, imports, runtimeVue).default.setup({}, { expose() {} }) - return { list: setup('index'), detail: setup('detail'), writes } + return { + list: setup('index'), + detail: setup('detail'), + writes, + confirmations, + messageBox: imports['element-plus'].ElMessageBox, + service + } } const validForm = { @@ -170,6 +188,113 @@ test('editing then creating resets control state; default creation sends numeric assert.equal(list.form.realname_control_enabled, 0) }) +for (const userType of [1, 2]) { + test(`user_type=${userType}: enabling control requires confirmation before PUT and sends true`, async () => { + const { list, writes, confirmations, messageBox } = harness(userType) + list.showDialog('edit', carrier(0)) + list.form.realname_control_enabled = 1 + let confirm + messageBox.confirm = (...args) => { + confirmations.push(args) + return new Promise((resolve) => { + confirm = resolve + }) + } + const pending = list.handleSubmit(validForm) + assert.equal(confirmations.length, 1) + assert.match(confirmations[0][0], /未实名的开机卡会在下一次评估时被停机/) + assert.deepEqual(writes, []) + assert.equal(list.submitLoading.value, true) + await list.handleSubmit(validForm) + assert.equal(confirmations.length, 1) + confirm('confirm') + await pending + assert.equal(writes.length, 1) + assert.equal(writes[0].method, 'update') + assert.equal(writes[0].data.realname_control_enabled, 1) + assert.equal(writes[0].data.realname_control_confirm, true) + assert.equal(list.submitLoading.value, false) + }) +} + +for (const reason of ['cancel', 'close']) { + test(`${reason} of realname confirmation does not submit; retry requires a fresh confirmation`, async () => { + const { list, writes, messageBox } = harness(1) + list.showDialog('edit', carrier(0)) + list.form.realname_control_enabled = 1 + let prompts = 0 + messageBox.confirm = async () => { + prompts += 1 + throw reason + } + await list.handleSubmit(validForm) + assert.deepEqual(writes, []) + assert.equal(list.dialogVisible.value, true) + assert.equal(list.submitLoading.value, false) + assert.equal(list.form.realname_control_enabled, 1) + messageBox.confirm = async () => { + prompts += 1 + return 'confirm' + } + await list.handleSubmit(validForm) + assert.equal(prompts, 2) + assert.equal(writes[0].data.realname_control_confirm, true) + }) +} + +test('unchanged, disabled, new, and unauthorized carriers never send confirmation automatically', async () => { + for (const [userType, mode, original, selected] of [ + [1, 'edit', 1, 1], + [1, 'edit', 0, 0], + [2, 'edit', 1, 0], + [1, 'add', 0, 1], + [3, 'edit', 0, 1], + [4, 'edit', 0, 1] + ]) { + const { list, writes, confirmations } = harness(userType) + list.showDialog(mode, carrier(original)) + list.form.realname_control_enabled = selected + await list.handleSubmit(validForm) + assert.equal(confirmations.length, 0) + assert.equal(writes.length, 1) + assert.equal(Object.hasOwn(writes[0].data, 'realname_control_confirm'), false) + } +}) + +test('opening another carrier resets the original control state after a cancelled enabling', async () => { + const { list, writes, messageBox } = harness(2) + list.showDialog('edit', carrier(0)) + list.form.realname_control_enabled = 1 + messageBox.confirm = async () => { + throw 'cancel' + } + await list.handleSubmit(validForm) + list.showDialog('edit', { ...carrier(1), id: 8 }) + await list.handleSubmit(validForm) + assert.equal(writes[0].id, 8) + assert.equal(Object.hasOwn(writes[0].data, 'realname_control_confirm'), false) +}) + +test('a failed PUT requires a fresh confirmation on retry', async (t) => { + const { list, writes, confirmations, service } = harness(2) + t.mock.method(console, 'error', () => {}) + list.showDialog('edit', carrier(0)) + list.form.realname_control_enabled = 1 + const updateCarrier = service.updateCarrier + service.updateCarrier = async () => { + throw new Error('PUT failed') + } + await list.handleSubmit(validForm) + assert.equal(confirmations.length, 1) + assert.deepEqual(writes, []) + assert.equal(list.dialogVisible.value, true) + assert.equal(list.submitLoading.value, false) + service.updateCarrier = updateCarrier + await list.handleSubmit(validForm) + assert.equal(confirmations.length, 2) + assert.equal(writes[0].data.realname_control_confirm, true) +}) + function findElement(node, predicate) { if (node.type === 1 && predicate(node)) return node for (const child of node.children || []) { @@ -235,11 +360,18 @@ test('carrier endpoints preserve the field in list/detail responses and create/u assert.equal((await CarrierService.getCarriers()).data.items[0].realname_control_enabled, 1) assert.equal((await CarrierService.getCarrierDetail(7)).data.realname_control_enabled, 0) await CarrierService.createCarrier({ ...carrier(1) }) - await CarrierService.updateCarrier(7, { realname_control_enabled: 0 }) + await CarrierService.updateCarrier(7, { + realname_control_enabled: 1, + realname_control_confirm: true + }) assert.deepEqual(calls, [ { method: 'GET', url: '/api/admin/carriers' }, { method: 'GET', url: '/api/admin/carriers/7' }, { method: 'POST', url: '/api/admin/carriers', data: carrier(1) }, - { method: 'PUT', url: '/api/admin/carriers/7', data: { realname_control_enabled: 0 } } + { + method: 'PUT', + url: '/api/admin/carriers/7', + data: { realname_control_enabled: 1, realname_control_confirm: true } + } ]) }) diff --git a/src/types/api/carrier.ts b/src/types/api/carrier.ts index a278c2f..f99b2e9 100644 --- a/src/types/api/carrier.ts +++ b/src/types/api/carrier.ts @@ -63,6 +63,7 @@ export interface UpdateCarrierParams { description?: string | null data_reset_day?: number | null // 上游流量重置日(1-28) realname_control_enabled?: CarrierRealnameControlEnabled // 通道实名管控 (0:不管控, 1:管控) + realname_control_confirm?: boolean // 从不管控改为管控时,确认未实名开机卡将在下次评估时被停机 realname_link_type?: string | null // 实名链接类型 realname_link_template?: string | null // 实名链接模板 traffic_threshold_enabled?: CarrierTrafficThresholdEnabled | null // 通道流量阈值开关 diff --git a/src/views/system/carrier-management/index.vue b/src/views/system/carrier-management/index.vue index 63a1d2b..a83885d 100644 --- a/src/views/system/carrier-management/index.vue +++ b/src/views/system/carrier-management/index.vue @@ -206,7 +206,13 @@ import { CarrierService } from '@/api/modules' import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus' - import type { Carrier, CarrierType, CreateCarrierParams } from '@/types/api' + import type { + Carrier, + CarrierType, + CarrierRealnameControlEnabled, + CreateCarrierParams, + UpdateCarrierParams + } from '@/types/api' import type { SearchFormItem } from '@/types' import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useAuth } from '@/composables/useAuth' @@ -592,6 +598,7 @@ } const dialogType = ref('add') + const originalRealnameControlEnabled = ref(0) // 显示新增/编辑对话框 const showDialog = (type: string, row?: any) => { @@ -606,6 +613,7 @@ form.description = row.description form.data_reset_day = row.data_reset_day || 1 form.realname_control_enabled = row.realname_control_enabled ?? 0 + originalRealnameControlEnabled.value = form.realname_control_enabled form.traffic_threshold_mode = getThresholdMode(row.traffic_threshold_enabled) form.traffic_threshold_unit = row.traffic_threshold_unit ?? null form.traffic_threshold_value = row.traffic_threshold_value ?? null @@ -619,6 +627,7 @@ form.description = '' form.data_reset_day = 1 form.realname_control_enabled = 0 + originalRealnameControlEnabled.value = 0 form.traffic_threshold_mode = 'unconfigured' form.traffic_threshold_unit = null form.traffic_threshold_value = null @@ -713,7 +722,7 @@ // 提交表单 const handleSubmit = async (formEl: FormInstance | undefined) => { - if (!formEl) return + if (!formEl || submitLoading.value) return if ( canConfigureTrafficThreshold.value && @@ -727,7 +736,7 @@ } await formEl.validate(async (valid) => { - if (valid) { + if (valid && !submitLoading.value) { submitLoading.value = true try { const data: CreateCarrierParams = { @@ -747,7 +756,7 @@ await CarrierService.createCarrier(data) ElMessage.success('新增成功') } else { - const updateData = { + const updateData: UpdateCarrierParams = { carrier_name: form.carrier_name, description: form.description || undefined, data_reset_day: form.data_reset_day || null, @@ -757,6 +766,27 @@ ...getTrafficThresholdPayload(), ...getRealnameControlPayload() } + if ( + canConfigureRealnameControl.value && + originalRealnameControlEnabled.value === 0 && + updateData.realname_control_enabled === 1 + ) { + try { + await ElMessageBox.confirm( + '开启通道实名管控后,该通道未实名的开机卡会在下一次评估时被停机。确认开启吗?', + '确认开启通道实名管控', + { + type: 'warning', + confirmButtonText: '确认开启', + cancelButtonText: '取消', + closeOnClickModal: false + } + ) + } catch { + return + } + updateData.realname_control_confirm = true + } await CarrierService.updateCarrier(form.id, updateData) ElMessage.success('修改成功') } @@ -798,6 +828,7 @@ display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 24px; + align-items: start; :deep(.el-form-item) { margin-right: 0; @@ -808,7 +839,7 @@ grid-column: 1 / -1; } - @media (max-width: 640px) { + @media (width <= 640px) { .carrier-form-grid { grid-template-columns: minmax(0, 1fr); }