diff --git a/frontend/src/views/inventory/WarehouseSettings.test.js b/frontend/src/views/inventory/WarehouseSettings.test.js
index fd017e3e..db45ef69 100644
--- a/frontend/src/views/inventory/WarehouseSettings.test.js
+++ b/frontend/src/views/inventory/WarehouseSettings.test.js
@@ -1,5 +1,5 @@
import { flushPromises, mount } from '@vue/test-utils'
-import ElementPlus, { ElMessage } from 'element-plus'
+import ElementPlus, { ElDialog, ElMessage } from 'element-plus'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import WarehouseSettings from './WarehouseSettings.vue'
import { warehousesApi } from '../../api/warehouses'
@@ -8,10 +8,26 @@ vi.mock('../../api/warehouses', () => ({
warehousesApi: {
list: vi.fn(),
create: vi.fn(),
- createLocation: vi.fn()
+ update: vi.fn(),
+ createLocation: vi.fn(),
+ updateLocation: vi.fn()
}
}))
+// 真实的 ElSelect 在 jsdom 下会因状态双向绑定反复触发自身更新而报
+// "Maximum recursive updates exceeded"。这里只关心状态下拉提交的内容,用轻量替身绕开。
+// 与 MasterDataSettings.test.js 里的同款处理保持一致。
+const SelectStub = {
+ props: ['modelValue', 'placeholder'],
+ emits: ['update:modelValue', 'change'],
+ template: '
'
+}
+
+const OptionStub = {
+ props: ['label', 'value'],
+ template: '{{ label }}'
+}
+
// 表单项没有 placeholder,按 label 文本定位输入框。
function inputByLabel(wrapper, label) {
const item = wrapper.findAll('.el-form-item').find(
@@ -24,19 +40,47 @@ function findButton(wrapper, text) {
return wrapper.findAll('button').find(button => button.text().includes(text))
}
+// 页面改造后「编辑」按钮会有多个(每个仓库一个、每个库位一个),
+// 必须把查找收窄到作用域里,不能再用全局的 findButton 取首个。
+function findButtonIn(scope, text) {
+ return scope.findAll('button').find(button => button.text().includes(text))
+}
+
async function clickSave(wrapper) {
await findButton(wrapper, '保存').trigger('click')
await flushPromises()
}
+const WAREHOUSE_WITH_LOCATION = {
+ data: [{
+ id: 1,
+ code: 'DEFAULT',
+ name: '默认仓库',
+ status: 'active',
+ locations: [{ id: 9, code: 'MAIN', name: '主库位', status: 'active' }]
+ }]
+}
+
+function mountPage() {
+ return mount(WarehouseSettings, {
+ global: {
+ plugins: [ElementPlus],
+ stubs: { teleport: true, ElSelect: SelectStub, ElOption: OptionStub }
+ }
+ })
+}
+
describe('仓库与库位表单必填校验', () => {
let warnSpy
beforeEach(() => {
warnSpy = vi.spyOn(ElMessage, 'warning').mockImplementation(() => {})
+ vi.spyOn(ElMessage, 'success').mockImplementation(() => {})
warehousesApi.list.mockReset().mockResolvedValue({ data: [] })
warehousesApi.create.mockReset().mockResolvedValue({ data: {} })
+ warehousesApi.update.mockReset().mockResolvedValue({ data: {} })
warehousesApi.createLocation.mockReset().mockResolvedValue({ data: {} })
+ warehousesApi.updateLocation.mockReset().mockResolvedValue({ data: {} })
})
afterEach(() => {
@@ -44,9 +88,7 @@ describe('仓库与库位表单必填校验', () => {
})
test('新增仓库时编码留空被拦下', async () => {
- const wrapper = mount(WarehouseSettings, {
- global: { plugins: [ElementPlus], stubs: { teleport: true } }
- })
+ const wrapper = mountPage()
await flushPromises()
await findButton(wrapper, '新增仓库').trigger('click')
@@ -59,9 +101,7 @@ describe('仓库与库位表单必填校验', () => {
})
test('只填了编码时提示名称', async () => {
- const wrapper = mount(WarehouseSettings, {
- global: { plugins: [ElementPlus], stubs: { teleport: true } }
- })
+ const wrapper = mountPage()
await flushPromises()
await findButton(wrapper, '新增仓库').trigger('click')
@@ -78,9 +118,7 @@ describe('仓库与库位表单必填校验', () => {
warehousesApi.list.mockResolvedValue({
data: [{ id: 1, code: 'DEFAULT', name: '默认仓库', status: 'active', locations: [] }]
})
- const wrapper = mount(WarehouseSettings, {
- global: { plugins: [ElementPlus], stubs: { teleport: true } }
- })
+ const wrapper = mountPage()
await flushPromises()
await findButton(wrapper, '新增库位').trigger('click')
@@ -93,9 +131,7 @@ describe('仓库与库位表单必填校验', () => {
})
test('编码和名称都填好后正常提交', async () => {
- const wrapper = mount(WarehouseSettings, {
- global: { plugins: [ElementPlus], stubs: { teleport: true } }
- })
+ const wrapper = mountPage()
await flushPromises()
await findButton(wrapper, '新增仓库').trigger('click')
@@ -109,3 +145,153 @@ describe('仓库与库位表单必填校验', () => {
wrapper.unmount()
})
})
+
+describe('仓库与库位停用/启用', () => {
+ let warnSpy
+
+ beforeEach(() => {
+ warnSpy = vi.spyOn(ElMessage, 'warning').mockImplementation(() => {})
+ vi.spyOn(ElMessage, 'success').mockImplementation(() => {})
+ warehousesApi.list.mockReset().mockResolvedValue(WAREHOUSE_WITH_LOCATION)
+ warehousesApi.create.mockReset().mockResolvedValue({ data: {} })
+ warehousesApi.update.mockReset().mockResolvedValue({ data: {} })
+ warehousesApi.createLocation.mockReset().mockResolvedValue({ data: {} })
+ warehousesApi.updateLocation.mockReset().mockResolvedValue({ data: {} })
+ })
+
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ test('编辑仓库改状态走 update,不走 create', async () => {
+ const wrapper = mountPage()
+ await flushPromises()
+
+ const header = wrapper.find('.el-collapse-item__header')
+ await findButtonIn(header, '编辑').trigger('click')
+ await flushPromises()
+ await inputByLabel(wrapper, '名称').setValue('一号仓')
+ await clickSave(wrapper)
+
+ // 编辑只发 name + status:code 不可变,后端 Update 也不接收
+ expect(warehousesApi.update).toHaveBeenCalledWith(1, { name: '一号仓', status: 'active' })
+ expect(warehousesApi.create).not.toHaveBeenCalled()
+ wrapper.unmount()
+ })
+
+ test('编辑态下把状态改成停用能提交', async () => {
+ const wrapper = mountPage()
+ await flushPromises()
+
+ await findButtonIn(wrapper.find('.el-collapse-item__header'), '编辑').trigger('click')
+ await flushPromises()
+ // 下拉里选「停用」:emit v-model 事件,跳过 popper 在 jsdom 下的定位问题
+ wrapper.findComponent(SelectStub).vm.$emit('update:modelValue', 'inactive')
+ await flushPromises()
+ await clickSave(wrapper)
+
+ expect(warehousesApi.update).toHaveBeenCalledWith(1, { name: '默认仓库', status: 'inactive' })
+ wrapper.unmount()
+ })
+
+ test('编辑态一个字段都不改也能直接保存,且不误报编码', async () => {
+ const wrapper = mountPage()
+ await flushPromises()
+
+ await findButtonIn(wrapper.find('.el-collapse-item__header'), '编辑').trigger('click')
+ await flushPromises()
+ await clickSave(wrapper)
+
+ // 编码在编辑态是只读且不参与校验,不该被「请填写编码」拦下
+ expect(warnSpy).not.toHaveBeenCalled()
+ expect(warehousesApi.update).toHaveBeenCalledWith(1, { name: '默认仓库', status: 'active' })
+ wrapper.unmount()
+ })
+
+ test('编辑过之后再点新增,走的仍是 create', async () => {
+ const wrapper = mountPage()
+ await flushPromises()
+
+ await findButtonIn(wrapper.find('.el-collapse-item__header'), '编辑').trigger('click')
+ await flushPromises()
+ await clickSave(wrapper)
+ expect(warehousesApi.update).toHaveBeenCalledTimes(1)
+
+ await findButton(wrapper, '新增仓库').trigger('click')
+ await flushPromises()
+ await inputByLabel(wrapper, '编码').setValue('WH-004')
+ await inputByLabel(wrapper, '名称').setValue('四号仓')
+ await clickSave(wrapper)
+
+ // 编辑残留的 id 若漏进 form,这里就会变成第二次 update(覆盖别人的记录)
+ expect(warehousesApi.create).toHaveBeenCalledWith({ code: 'WH-004', name: '四号仓', status: 'active' })
+ expect(warehousesApi.update).toHaveBeenCalledTimes(1)
+ wrapper.unmount()
+ })
+
+ test('编辑库位的参数顺序是 (仓库 id, 库位 id, 内容)', async () => {
+ const wrapper = mountPage()
+ await flushPromises()
+
+ const row = wrapper.find('.el-table__row')
+ expect(row.exists()).toBe(true)
+ await findButtonIn(row, '编辑').trigger('click')
+ await flushPromises()
+ await inputByLabel(wrapper, '名称').setValue('主库位A')
+ await clickSave(wrapper)
+
+ // 顺序写反会打到 /warehouses/{库位id}/locations/undefined
+ expect(warehousesApi.updateLocation).toHaveBeenCalledWith(1, 9, { name: '主库位A', status: 'active' })
+ wrapper.unmount()
+ })
+
+ test('库位状态列显示中文而不是英文原值', async () => {
+ warehousesApi.list.mockResolvedValue({
+ data: [{
+ id: 1,
+ code: 'DEFAULT',
+ name: '默认仓库',
+ status: 'inactive',
+ locations: [{ id: 9, code: 'MAIN', name: '主库位', status: 'inactive' }]
+ }]
+ })
+ const wrapper = mountPage()
+ await flushPromises()
+
+ const text = wrapper.text()
+ // 先确认库位行真的渲染出来了,否则下面的断言会假通过
+ expect(text).toContain('主库位')
+ expect(text).toContain('停用')
+ expect(text).not.toContain('inactive')
+ wrapper.unmount()
+ })
+
+ test('点标题栏编辑不会连带把面板收起来', async () => {
+ const wrapper = mountPage()
+ await flushPromises()
+
+ await wrapper.find('.el-collapse-item__header').trigger('click')
+ await flushPromises()
+ expect(wrapper.find('.el-collapse-item__header').classes()).toContain('is-active')
+
+ await findButtonIn(wrapper.find('.el-collapse-item__header'), '编辑').trigger('click')
+ await flushPromises()
+
+ expect(wrapper.find('.el-collapse-item__header').classes()).toContain('is-active')
+ wrapper.unmount()
+ })
+
+ test('保存失败时弹窗不自动关闭', async () => {
+ warehousesApi.update.mockRejectedValueOnce(new Error('接口炸了'))
+ const wrapper = mountPage()
+ await flushPromises()
+
+ await findButtonIn(wrapper.find('.el-collapse-item__header'), '编辑').trigger('click')
+ await flushPromises()
+ await clickSave(wrapper)
+
+ expect(warehousesApi.update).toHaveBeenCalledTimes(1)
+ expect(wrapper.findComponent(ElDialog).props('modelValue')).toBe(true)
+ wrapper.unmount()
+ })
+})
diff --git a/frontend/src/views/inventory/WarehouseSettings.vue b/frontend/src/views/inventory/WarehouseSettings.vue
index 15530344..7e989ed8 100644
--- a/frontend/src/views/inventory/WarehouseSettings.vue
+++ b/frontend/src/views/inventory/WarehouseSettings.vue
@@ -3,19 +3,41 @@
仓库与库位属于库存域,不在物料主数据中维护。新增仓库
- {{ warehouse.code }} - {{ warehouse.name }}{{ warehouse.status === 'active' ? '启用' : '停用' }}
-
+
+ {{ warehouse.code }} - {{ warehouse.name }}
+ {{ warehouse.status === 'active' ? '启用' : '停用' }}
+
+ 编辑
+
+
-
+
+
+
+
+ {{ row.status === 'active' ? '启用' : '停用' }}
+
+
+
+
+ 编辑
+
+
-
+
-
+
+
+
+
+
+
+
取消保存
@@ -23,25 +45,83 @@