fix: 审批/取消后列表的筛选条件被丢弃

写操作后原地重新拉取列表时,status 参数被写死成 '':列表悄悄退回全量(已通过、
已拒绝的单子全冒出来),而页面下拉框仍显示着用户选中的筛选值。用户以为看到的
是筛选结果,实际不是。store 层一直有这个缺陷却没被拦住,因为页面那层没有测试。

修法照 stock.js 已有的约定(stockFilters 配合 stock.test.js 的断言):store 存一份
filters,写操作原地刷新时取出来用。

- approval.js:approve / reject 沿用筛选
- application.js:cancel 沿用筛选
- application.js:create 保持清空筛选并回第 1 页——刚提交的是待审批单,若沿用
  「已通过」之类的筛选就看不见它,用户会以为没提交成功
- Application.vue:提交成功后同步把下拉框重置为「全部」,否则 store 清了筛选而
  下拉没清,同样是表里不一

测试补 9 条(approval/application 两个 store + Application 页面层)。全部做过变异
验证:把实现改回原样后对应断言确实变红,且只有该变红的那几条变红。
This commit is contained in:
文睿 张 2026-09-22 14:10:52 +08:00
parent bf7f7129dc
commit fbd0bc1341
6 changed files with 260 additions and 3 deletions

View File

@ -7,7 +7,12 @@ export const useApplicationStore = defineStore('application', () => {
const loading = ref(false)
const myPage = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
// 列表当前生效的筛选条件。取消申请后会在原地重新拉取列表,那次拉取必须沿用
// 用户选中的状态,否则列表会悄悄退回全量,而下拉框里还显示着选中的筛选值。
const filters = ref({ status: '' })
async function fetchMyApplications(status = '', pageNum = 1) {
filters.value = { status }
loading.value = true
try {
const res = await applicationApi.getMy(status, pageNum, myPage.value.pageSize)
@ -22,13 +27,16 @@ export const useApplicationStore = defineStore('application', () => {
async function createApplication(data) {
const res = await applicationApi.create(data)
// 这处刻意清空筛选并回到第 1 页,和上面「沿用筛选」的规矩相反:刚提交的是
// 待审批单,若沿用「已通过」之类的筛选就看不见它,用户会以为没提交成功。
// 清空的是 store 这份状态,页面那份(下拉框)由 Application.vue 同步重置。
await fetchMyApplications('', 1)
return res
}
async function cancelApplication(id) {
const res = await applicationApi.cancel(id)
await fetchMyApplications('', myPage.value.currentPage)
await fetchMyApplications(filters.value.status, myPage.value.currentPage)
return res
}

View File

@ -0,0 +1,63 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { applicationApi } from '../api/application'
import { useApplicationStore } from './application'
vi.mock('../api/application', () => ({
applicationApi: {
getAll: vi.fn(),
getMy: vi.fn(),
create: vi.fn(),
cancel: vi.fn(),
approve: vi.fn(),
reject: vi.fn()
}
}))
describe('application store 的筛选保持', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
applicationApi.getMy.mockResolvedValue({ data: { items: [], total: 0 } })
applicationApi.create.mockResolvedValue({ data: {} })
applicationApi.cancel.mockResolvedValue({ data: {} })
})
// 取消申请会在原地重新拉取列表,那次拉取必须沿用用户选中的状态:否则列表会悄悄
// 退回全量(已通过/已拒绝的单子全冒出来),而下拉框里还显示着「待审批」。
it('取消申请后刷新列表要沿用当前的筛选条件', async () => {
const store = useApplicationStore()
await store.fetchMyApplications('pending', 2)
applicationApi.getMy.mockClear()
await store.cancelApplication(3)
expect(applicationApi.getMy).toHaveBeenCalledWith('pending', 2, 20)
})
// 这处刻意与上面相反:刚提交的是待审批单,若沿用「已通过」之类的筛选就看不见它,
// 用户会以为没提交成功。所以清空筛选并回到第 1 页,让新单排在列表最前面。
it('提交新申请后要清空筛选并回到第 1 页', async () => {
const store = useApplicationStore()
await store.fetchMyApplications('approved', 3)
applicationApi.getMy.mockClear()
await store.createApplication({ purpose: '测试' })
expect(applicationApi.getMy).toHaveBeenCalledWith('', 1, 20)
})
// 首次加载没有筛选条件,别把上一次的筛选状态带到新页面上。
it('不带筛选条件地重新查询会清掉旧的筛选状态', async () => {
const store = useApplicationStore()
await store.fetchMyApplications('rejected', 1)
applicationApi.getMy.mockClear()
await store.fetchMyApplications()
expect(applicationApi.getMy).toHaveBeenCalledWith('', 1, 20)
})
})

View File

@ -7,7 +7,12 @@ export const useApprovalStore = defineStore('approval', () => {
const loading = ref(false)
const page = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
// 列表当前生效的筛选条件。审批通过/拒绝后会在原地重新拉取列表,那次拉取必须
// 沿用用户选中的状态,否则列表会悄悄退回全量,而下拉框里还显示着选中的筛选值。
const filters = ref({ status: '' })
async function fetchPendingApplications(status = '', pageNum = 1) {
filters.value = { status }
loading.value = true
try {
const res = await applicationApi.getAll(status, pageNum, page.value.pageSize)
@ -22,13 +27,13 @@ export const useApprovalStore = defineStore('approval', () => {
async function approveApplication(id, data = {}) {
const res = await applicationApi.approve(id, data)
await fetchPendingApplications('', page.value.currentPage)
await fetchPendingApplications(filters.value.status, page.value.currentPage)
return res
}
async function rejectApplication(id, data = {}) {
const res = await applicationApi.reject(id, data)
await fetchPendingApplications('', page.value.currentPage)
await fetchPendingApplications(filters.value.status, page.value.currentPage)
return res
}

View File

@ -0,0 +1,70 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { applicationApi } from '../api/application'
import { useApprovalStore } from './approval'
vi.mock('../api/application', () => ({
applicationApi: {
getAll: vi.fn(),
getMy: vi.fn(),
create: vi.fn(),
cancel: vi.fn(),
approve: vi.fn(),
reject: vi.fn()
}
}))
describe('approval store 的筛选保持', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
applicationApi.getAll.mockResolvedValue({ data: { items: [], total: 0 } })
applicationApi.approve.mockResolvedValue({ data: {} })
applicationApi.reject.mockResolvedValue({ data: {} })
})
// 审批会在原地重新拉取列表,那次拉取必须沿用用户选中的状态:否则列表会悄悄退回
// 全量(已通过/已拒绝的单子全冒出来),而下拉框里还显示着「待处理」。
it('审批通过后刷新列表要沿用当前的筛选条件', async () => {
const store = useApprovalStore()
await store.fetchPendingApplications('pending', 2)
applicationApi.getAll.mockClear()
await store.approveApplication(9, { auto_outbound: true })
expect(applicationApi.getAll).toHaveBeenCalledWith('pending', 2, 20)
})
it('审批拒绝后刷新列表要沿用当前的筛选条件', async () => {
const store = useApprovalStore()
await store.fetchPendingApplications('pending', 1)
applicationApi.getAll.mockClear()
await store.rejectApplication(9, { reason: '库存不足' })
expect(applicationApi.getAll).toHaveBeenCalledWith('pending', 1, 20)
})
// 首次加载没有筛选条件,别把上一次的筛选状态带到新页面上。
it('不带筛选条件地重新查询会清掉旧的筛选状态', async () => {
const store = useApprovalStore()
await store.fetchPendingApplications('approved', 1)
applicationApi.getAll.mockClear()
await store.fetchPendingApplications()
expect(applicationApi.getAll).toHaveBeenCalledWith('', 1, 20)
})
// 翻页本身也要带上筛选:漏掉的话第 2 页会突然冒出全量数据。
it('翻页时带上当前的筛选条件', async () => {
const store = useApprovalStore()
await store.fetchPendingApplications('approved', 3)
expect(applicationApi.getAll).toHaveBeenCalledWith('approved', 3, 20)
})
})

View File

@ -0,0 +1,108 @@
import { flushPromises, mount } from '@vue/test-utils'
import ElementPlus, { ElMessage } from 'element-plus'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import Application from './Application.vue'
const applicationStore = vi.hoisted(() => ({
myApplications: [],
loading: false,
myPage: { currentPage: 1, pageSize: 20, total: 0, totalPages: 0 },
fetchMyApplications: vi.fn(),
createApplication: vi.fn(),
cancelApplication: vi.fn()
}))
const materialsStore = vi.hoisted(() => ({
materialOptions: [],
fetchMaterialOptions: vi.fn()
}))
const stockStore = vi.hoisted(() => ({
fetchStock: vi.fn(),
fetchStockByMaterialId: vi.fn()
}))
vi.mock('../stores/application', () => ({ useApplicationStore: () => applicationStore }))
vi.mock('../stores/materials', () => ({ useMaterialsStore: () => materialsStore }))
vi.mock('../stores/stock', () => ({ useStockStore: () => stockStore }))
// 真实的 ElSelect 在 jsdom 下会因状态双向绑定反复触发自身更新而报
// "Maximum recursive updates exceeded"。这里用替身直接派发 change,
// 模拟用户在状态下拉里选中某一项。
const SelectStub = {
props: ['modelValue', 'placeholder', 'filterable', 'filterMethod', 'noDataText', 'disabled', 'clearable'],
emits: ['update:modelValue', 'change'],
methods: {
pick(value) {
this.$emit('update:modelValue', value)
this.$emit('change', value)
}
},
template: '<div class="select-stub"></div>'
}
const OptionStub = { props: ['label', 'value'], template: '<span />' }
function mountApplication() {
return mount(Application, {
global: {
plugins: [ElementPlus],
stubs: { teleport: true, ElSelect: SelectStub, ElOption: OptionStub }
}
})
}
// 页面里有两处 ElSelect:申请表单里的物料选择,和「我的申请」里的状态筛选。
function statusSelect(wrapper) {
return wrapper.findAllComponents(SelectStub)
.find(select => select.props('placeholder') === '全部状态')
}
function setupState(wrapper) {
return wrapper.vm.$.setupState
}
describe('提交申请后筛选条件与列表保持一致', () => {
let successSpy
beforeEach(() => {
vi.clearAllMocks()
applicationStore.createApplication.mockResolvedValue({ data: {} })
successSpy = vi.spyOn(ElMessage, 'success').mockImplementation(() => {})
})
test('状态下拉选中的值会传给列表查询', async () => {
const wrapper = mountApplication()
await flushPromises()
statusSelect(wrapper).vm.pick('approved')
await flushPromises()
expect(setupState(wrapper).statusFilter).toBe('approved')
expect(applicationStore.fetchMyApplications).toHaveBeenLastCalledWith('approved', 1)
wrapper.unmount()
})
// store 里提交后的那次刷新是刻意清空筛选的(好让刚提交的待审批单出现在列表里),
// 页面这份状态(下拉框)必须跟着一起回到「全部」:否则会出现下拉写着「已通过」、
// 列表却是全量的表里不一,这次修复就是补的这一处。
test('提交成功后下拉框的筛选要一起重置为全部', async () => {
const wrapper = mountApplication()
await flushPromises()
statusSelect(wrapper).vm.pick('approved')
await flushPromises()
expect(setupState(wrapper).statusFilter).toBe('approved')
setupState(wrapper).applyForm.purpose = '测试用途'
setupState(wrapper).applyForm.items = [
{ material_id: 7, materialSearchText: '', quantity: 1 }
]
await setupState(wrapper).handleSubmit()
await flushPromises()
expect(applicationStore.createApplication).toHaveBeenCalled()
expect(setupState(wrapper).statusFilter).toBe('')
wrapper.unmount()
})
})

View File

@ -212,6 +212,9 @@ async function handleSubmit() {
applyForm.purpose = ''
applyForm.items = [{ material_id: null, materialSearchText: '', quantity: 1 }]
activeTab.value = 'my'
// store 里提交后的那次刷新会清空筛选(好让刚提交的待审批单出现在列表里),
// 下拉框得跟着一起回到「全部」,否则会出现下拉写着「已通过」、列表却是全量。
statusFilter.value = ''
} finally {
submitLoading.value = false
}