bugs修复

This commit is contained in:
文睿 张 2026-09-23 14:46:51 +08:00
parent d62d6777d1
commit 7f06b2ee58
4 changed files with 90 additions and 8 deletions

View File

@ -42,7 +42,7 @@ const SelectStub = {
template: '<div class="select-stub"></div>'
}
const OptionStub = { props: ['label', 'value'], template: '<span />' }
const OptionStub = { props: ['label', 'value', 'disabled'], template: '<span />' }
const categories = [
{ id: 1, code: 'A', name: '机械结构类', parent_id: null, status: 'active' },
@ -380,3 +380,38 @@ describe('物料列表的作废入口文案', () => {
wrapper.unmount()
})
})
describe('停用分类下的物料编辑', () => {
test('保留原分类并显示停用状态,修改名称时不改变分类归属', async () => {
materialCategoriesApi.list.mockResolvedValue({ data: [
{ id: 1, code: 'A', name: '旧大类', parent_id: null, status: 'inactive' },
{ id: 12, code: 'A01', name: '旧小类', parent_id: 1, status: 'inactive' },
{ id: 2, code: 'B', name: '新大类', parent_id: null, status: 'active' },
{ id: 21, code: 'B01', name: '新小类', parent_id: 2, status: 'active' }
] })
unitsApi.list.mockResolvedValue({ data: [] })
materialsStore.updateMaterial.mockReset().mockResolvedValue({ data: {} })
vi.spyOn(ElMessage, 'success').mockImplementation(() => {})
const wrapper = mountMaterials()
await flushPromises()
const state = setupState(wrapper)
const row = { id: 7, category_id: 12, category: 'A01', material_code: 'MAT-7', name: '原名称' }
expect(state.getMajorCategoryDisplay(row)).toBe('A 旧大类')
expect(state.getMinorCategoryDisplay(row)).toBe('旧小类')
state.showEditModal(row)
await flushPromises()
expect(state.form.major_category_id).toBe(1)
expect(state.form.category_id).toBe(12)
expect(state.majorCategories.map(category => category.id)).toContain(1)
expect(state.minorCategories.map(category => category.id)).toContain(12)
expect(state.categoryLabel(state.minorCategories.find(category => category.id === 12))).toBe('旧小类(已停用)')
state.form.name = '新名称'
await state.handleSave()
expect(materialsStore.updateMaterial).toHaveBeenCalledWith(7, expect.objectContaining({ category_id: 12, name: '新名称' }))
wrapper.unmount()
})
})

View File

@ -88,12 +88,12 @@
</el-form-item>
<el-form-item label="大类类型" required>
<el-select v-model="form.major_category_id" style="width: 100%" @change="syncMajorCategory">
<el-option v-for="category in majorCategories" :key="category.id" :label="`${category.code} ${category.name}`" :value="category.id" />
<el-option v-for="category in majorCategories" :key="category.id" :label="categoryLabel(category, true)" :value="category.id" :disabled="category.status !== 'active'" />
</el-select>
</el-form-item>
<el-form-item label="小类类型" required>
<el-select v-model="form.category_id" :disabled="!form.major_category_id" style="width: 100%" @change="syncCategory">
<el-option v-for="category in minorCategories" :key="category.id" :label="category.name" :value="category.id" />
<el-option v-for="category in minorCategories" :key="category.id" :label="categoryLabel(category)" :value="category.id" :disabled="category.status !== 'active'" />
</el-select>
</el-form-item>
<el-form-item label="单位">
@ -203,8 +203,21 @@ const codePlaceholder = computed(() => {
if (!isCreating.value || form.category_id) return ''
return '请先选择大类类型与小类类型'
})
const majorCategories = computed(() => getMajorCategories(categories.value))
const minorCategories = computed(() => getMinorCategories(categories.value, form.major_category_id))
const majorCategories = computed(() => {
const active = getMajorCategories(categories.value)
const selected = categories.value.find(item => item.id === form.major_category_id)
return selected && !active.some(item => item.id === selected.id) ? [...active, selected] : active
})
const minorCategories = computed(() => {
const active = getMinorCategories(categories.value, form.major_category_id)
const selected = categories.value.find(item => item.id === form.category_id && item.parent_id === form.major_category_id)
return selected && !active.some(item => item.id === selected.id) ? [...active, selected] : active
})
function categoryLabel(category, withCode = false) {
const name = withCode ? `${category.code} ${category.name}` : category.name
return category.status === 'active' ? name : `${name}(已停用)`
}
function handleImageUpload(e) {
const file = e.target.files[0]
@ -222,7 +235,7 @@ onMounted(async () => {
// 业务单据页(入库/出库/申请)仍用只有有效物料的 materialOptions。
materialsStore.fetchHistoricalMaterialOptions()
const [categoryResponse, unitResponse] = await Promise.all([materialCategoriesApi.list(), unitsApi.list()])
categories.value = (categoryResponse.data || []).filter(item => item.status === 'active')
categories.value = categoryResponse.data || []
units.value = (unitResponse.data || []).filter(item => item.status === 'active')
})

View File

@ -137,8 +137,13 @@ func (s *MaterialService) Update(id int64, req *model.UpdateMaterialRequest, ope
if err != nil {
return nil, err
}
if err := s.validateCategory(req.CategoryID, oldMaterial.OrganizationID); err != nil {
return nil, err
if req.CategoryID == nil || *req.CategoryID <= 0 {
return nil, ErrInvalidMaterialCategory
}
if oldMaterial.CategoryID == nil || *req.CategoryID != *oldMaterial.CategoryID {
if err := s.validateCategory(req.CategoryID, oldMaterial.OrganizationID); err != nil {
return nil, err
}
}
err = s.repo.Update(id, req)

View File

@ -77,6 +77,35 @@ func TestMaterialServiceTreatsObsoleteMaterialAsInactive(t *testing.T) {
}
}
func TestMaterialServiceUpdatesWithoutChangingDisabledCategory(t *testing.T) {
mock, cleanup := newMaterialServiceMock(t)
t.Cleanup(cleanup)
now := time.Date(2026, 9, 20, 9, 0, 0, 0, time.UTC)
materialRows := func(name string) *sqlmock.Rows {
return sqlmock.NewRows([]string{"id", "organization_id", "material_code", "name", "material_image", "spec", "manufacturer_model", "category_id", "base_unit_id", "unit", "status", "version", "created_at", "updated_at"}).
AddRow(7, 1, "MAT-007", name, nil, "", nil, 12, nil, "pcs", "active", 1, now, now)
}
mock.ExpectQuery(regexp.QuoteMeta(getMaterialQuery)).WithArgs(int64(7)).WillReturnRows(materialRows("原名称"))
mock.ExpectExec(regexp.QuoteMeta("UPDATE materials SET name = ?, category_id = ?, version = version + 1 WHERE id = ?")).
WithArgs("新名称", int64(12), int64(7)).WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectQuery(regexp.QuoteMeta(getMaterialQuery)).WithArgs(int64(7)).WillReturnRows(materialRows("新名称"))
mock.ExpectExec(regexp.QuoteMeta(materialLogStmt)).
WithArgs("UPDATE", int64(7), sqlmock.AnyArg(), sqlmock.AnyArg(), "tester", sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
categoryID := int64(12)
material, err := NewMaterialService().Update(7, &model.UpdateMaterialRequest{Name: "新名称", CategoryID: &categoryID}, "tester")
if err != nil {
t.Fatalf("update material with unchanged disabled category: %v", err)
}
if material.CategoryID == nil || *material.CategoryID != 12 || material.Name != "新名称" {
t.Fatalf("updated material = %+v, want category 12 and new name", material)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
// 以下常量必须与 repository 里的 SQL 字面量逐字一致,sqlmock 才匹配得上。
const (
activeSubcategoryQuery = `SELECT EXISTS(