From 8a60e2cb0033e3011a2e5df140267cf8371a502c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=87=E7=9D=BF=20=E5=BC=A0?= Date: Sun, 20 Sep 2026 17:17:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=8A=A5=E9=94=99=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/views/Materials.test.js | 30 +++++++++++++++++--- frontend/src/views/Materials.vue | 41 ++++++++++++---------------- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/frontend/src/views/Materials.test.js b/frontend/src/views/Materials.test.js index f1ea8586..8c590fb2 100644 --- a/frontend/src/views/Materials.test.js +++ b/frontend/src/views/Materials.test.js @@ -1,5 +1,5 @@ import { flushPromises, mount } from '@vue/test-utils' -import ElementPlus, { ElMessage } from 'element-plus' +import ElementPlus, { ElMessage, ElMessageBox } from 'element-plus' import { beforeEach, describe, expect, test, vi } from 'vitest' import Materials from './Materials.vue' import { codeRulesApi, materialCategoriesApi, unitsApi } from '../api/masterData' @@ -84,6 +84,7 @@ async function pickCategories(wrapper, { major = 1, minor = 12 } = {}) { describe('新增物料时的编码联动', () => { let warnSpy + let alertSpy beforeEach(() => { materialCategoriesApi.list.mockResolvedValue({ data: categories }) @@ -92,6 +93,7 @@ describe('新增物料时的编码联动', () => { materialsStore.createMaterial.mockReset() materialsStore.updateMaterial.mockReset() warnSpy = vi.spyOn(ElMessage, 'warning').mockImplementation(() => {}) + alertSpy = vi.spyOn(ElMessageBox, 'alert').mockResolvedValue('confirm') }) test('还没选小类时,编码框提示先选分类', async () => { @@ -119,11 +121,11 @@ describe('新增物料时的编码联动', () => { expect(codeRulesApi.next).toHaveBeenCalledWith(12) expect(codeInput(wrapper).element.value).toBe('A010201') - expect(wrapper.find('.material-code-hint').exists()).toBe(false) + expect(alertSpy).not.toHaveBeenCalled() wrapper.unmount() }) - test('小类没有规则时清空编码并提示手工填写', async () => { + test('小类没有规则时清空编码,并弹出需要点掉的提示框', async () => { codeRulesApi.next.mockResolvedValue({ data: { available: false, code: '' } }) const wrapper = mountMaterials() @@ -132,7 +134,11 @@ describe('新增物料时的编码联动', () => { await pickCategories(wrapper) expect(codeInput(wrapper).element.value).toBe('') - expect(wrapper.find('.material-code-hint').text()).toContain('请手工填写') + expect(alertSpy).toHaveBeenCalledWith( + '该小类未配置编码规则,请手工填写物料编码', + '提示', + expect.objectContaining({ confirmButtonText: '知道了' }) + ) wrapper.unmount() }) @@ -193,6 +199,22 @@ describe('新增物料时的编码联动', () => { wrapper.unmount() }) + test('名称为空时保存被拦下并提示补名称', async () => { + codeRulesApi.next.mockResolvedValue({ data: { available: true, code: 'A010201' } }) + + const wrapper = mountMaterials() + await flushPromises() + await openAddDialog(wrapper) + await pickCategories(wrapper) + + await findButton(wrapper, '保存').trigger('click') + await flushPromises() + + expect(materialsStore.createMaterial).not.toHaveBeenCalled() + expect(warnSpy).toHaveBeenCalledWith('请填写物料名称') + wrapper.unmount() + }) + test('小类没有规则且没填编码时,保存被拦下并提示补编码', async () => { codeRulesApi.next.mockResolvedValue({ data: { available: false, code: '' } }) diff --git a/frontend/src/views/Materials.vue b/frontend/src/views/Materials.vue index eef1b20c..0b203f8b 100644 --- a/frontend/src/views/Materials.vue +++ b/frontend/src/views/Materials.vue @@ -79,9 +79,8 @@ /> 重新生成 -

{{ codeHint }}

- + @@ -159,7 +158,7 @@ import { useAuthStore } from '../stores/auth' import BaseTable from '../components/business/BaseTable.vue' import DeleteConfirm from '../components/business/DeleteConfirm.vue' import AlertTip from '../components/business/AlertTip.vue' -import { ElMessage } from 'element-plus' +import { ElMessage, ElMessageBox } from 'element-plus' import { createMaterialForm, toMaterialPayload } from './materialForm' import { codeRulesApi, materialCategoriesApi, unitsApi } from '../api/masterData' import { getMajorCategories, getMinorCategories, resolveCategoryPath } from '../utils/materialCategories' @@ -187,7 +186,6 @@ const categories = ref([]) const units = ref([]) // 用户是否手改过编码:没改过时切分类要按新规则重算,改过就以用户的值为准。 const codeEdited = ref(false) -const codeHint = ref('') const isCreating = computed(() => dialogTitle.value === '新增物料') // 小类还没选时编码框必然是空的,提示用户先把分类选完:选完才会自动填入编码, // 或者由"请手工填写"的提示接手。 @@ -227,16 +225,14 @@ function syncMajorCategory() { // 大类换了,原来的小类规则不再适用;同样不动用户手改过的编码 if (!codeEdited.value) { form.material_code = '' - codeHint.value = '' } } -// 按当前小类的编码规则填编码:有规则自动填,没规则就清空并提示手工填写。 +// 按当前小类的编码规则填编码:有规则自动填,没规则就清空并弹窗让用户手工填写。 async function generateMaterialCode() { if (!isCreating.value) return if (!form.category_id) { form.material_code = '' - codeHint.value = '' return } try { @@ -244,15 +240,18 @@ async function generateMaterialCode() { const { available, code } = response.data || {} if (available) { form.material_code = code - codeHint.value = '' } else { form.material_code = '' - codeHint.value = '该小类未配置编码规则,请手工填写物料编码' + // 用模态框而不是会自动消失的 toast:这条提示要等用户点掉才走 + ElMessageBox.alert('该小类未配置编码规则,请手工填写物料编码', '提示', { + confirmButtonText: '知道了', + type: 'warning' + // ESC 或点遮罩关掉时 alert 会 reject,这里不做处理 + }).catch(() => {}) } } catch (e) { - // 预览失败不该挡住填表:留空让用户手工填写,接口层已经弹过错误提示 + // 预览失败不该挡住填表:留空让用户手工填写,失败原因由拦截器提示 form.material_code = '' - codeHint.value = '未能获取编码规则,请手工填写物料编码' } } @@ -305,7 +304,6 @@ function showAddModal() { dialogTitle.value = '新增物料' Object.assign(form, createMaterialForm()) codeEdited.value = false - codeHint.value = '' imagePreview.value = '' if (imageInput.value) imageInput.value.value = '' dialogVisible.value = true @@ -322,7 +320,6 @@ function showEditModal(row) { Object.assign(form, createMaterialForm(row, categories.value)) // 编辑时编码只读,标记成"已改过",避免切小类时被规则覆盖 codeEdited.value = true - codeHint.value = '' imagePreview.value = '' if (imageInput.value) imageInput.value.value = '' dialogVisible.value = true @@ -338,6 +335,11 @@ async function handleSave() { ElMessage.warning('请填写物料编码') return } + // 后端对名称也标了 binding:"required",前端先拦下来,避免抛回原始校验错误 + if (!form.name.trim()) { + ElMessage.warning('请填写物料名称') + return + } saveLoading.value = true try { const formData = toMaterialPayload(form) @@ -444,21 +446,14 @@ function formatDate(dateStr) { :deep(.el-table__cell.operation-column) { border-left: 1px solid var(--el-table-border-color) !important; } +/* 一行里横排「输入框 + 重新生成」;作为表单项内容区的 flex item,flex:1 让它 + 占满整行 —— 不设的话会收缩到 el-input 的固有宽度(约 177px),比表单里其它 + 字段窄一截。 */ .material-code-field { display: flex; align-items: center; gap: 8px; -} -/* 不设 flex 时这一行会收缩到 el-input 的固有宽度(约 177px),比表单里其它 - 字段窄一截;换成 flex:1 后输入框占满整行剩余宽度,与其它字段对齐。 */ -.material-code-field { flex: 1; min-width: 0; } -.material-code-hint { - margin: 4px 0 0; - color: #e6a23c; - font-size: 12px; - line-height: 1.5; -}