diff --git a/frontend/src/api/partners.js b/frontend/src/api/partners.js index 3dcef455..23db3922 100644 --- a/frontend/src/api/partners.js +++ b/frontend/src/api/partners.js @@ -1,8 +1,8 @@ import request from './index' export const partnersApi = { - list(role) { - return request.get('/partners', { params: { role } }) + list(role, params = {}) { + return request.get('/partners', { params: { role, ...params } }) }, create(data) { return request.post('/partners', data) diff --git a/frontend/src/components/business/BaseTable.vue b/frontend/src/components/business/BaseTable.vue index a31c7fe1..39af57d4 100644 --- a/frontend/src/components/business/BaseTable.vue +++ b/frontend/src/components/business/BaseTable.vue @@ -9,7 +9,7 @@ > { beforeEach(() => { warnSpy = vi.spyOn(ElMessage, 'warning').mockImplementation(() => {}) - partnersApi.list.mockReset().mockResolvedValue({ data: [] }) + partnersApi.list.mockReset().mockResolvedValue({ data: { items: [], total: 0 } }) partnersApi.create.mockReset().mockResolvedValue({ data: {} }) partnersApi.update.mockReset().mockResolvedValue({ data: {} }) }) @@ -126,7 +126,7 @@ describe('伙伴表单必填校验', () => { describe('伙伴列表的合作状态显示', () => { beforeEach(() => { - partnersApi.list.mockReset().mockResolvedValue({ data: [] }) + partnersApi.list.mockReset().mockResolvedValue({ data: { items: [], total: 0 } }) }) afterEach(() => { @@ -135,7 +135,7 @@ describe('伙伴列表的合作状态显示', () => { test('合作状态列显示中文,不把英文枚举值直接给用户', async () => { partnersApi.list.mockResolvedValue({ - data: [{ id: 1, partner_code: 'SUP-001', legal_name: '某某钢材有限公司', supplier_status: 'active' }] + data: { items: [{ id: 1, partner_code: 'SUP-001', legal_name: '某某钢材有限公司', supplier_status: 'active' }], total: 1 } }) const wrapper = mountPartner() await flushPromises() @@ -147,7 +147,7 @@ describe('伙伴列表的合作状态显示', () => { test('未收录的状态原样显示,不伪装成已知状态', async () => { partnersApi.list.mockResolvedValue({ - data: [{ id: 2, partner_code: 'SUP-002', legal_name: '某某轴承厂', supplier_status: 'unknown_state' }] + data: { items: [{ id: 2, partner_code: 'SUP-002', legal_name: '某某轴承厂', supplier_status: 'unknown_state' }], total: 1 } }) const wrapper = mountPartner() await flushPromises() @@ -160,7 +160,7 @@ describe('伙伴列表的合作状态显示', () => { describe('新增伙伴不发生表单残留', () => { beforeEach(() => { partnersApi.list.mockReset().mockResolvedValue({ - data: [{ id: 1, partner_code: 'SUP-001', legal_name: '某某钢材有限公司', supplier_status: 'active' }] + data: { items: [{ id: 1, partner_code: 'SUP-001', legal_name: '某某钢材有限公司', supplier_status: 'active' }], total: 1 } }) partnersApi.create.mockReset().mockResolvedValue({ data: {} }) partnersApi.update.mockReset().mockResolvedValue({ data: {} }) @@ -191,3 +191,57 @@ describe('新增伙伴不发生表单残留', () => { wrapper.unmount() }) }) + +describe('伙伴列表分页', () => { + beforeEach(() => { + partnersApi.list.mockReset() + }) + + afterEach(() => { vi.restoreAllMocks() }) + + test('首次加载取第一页,offset 从 0 开始', async () => { + partnersApi.list.mockResolvedValue({ data: { items: [], total: 0 } }) + const wrapper = mountPartner() + await flushPromises() + + expect(partnersApi.list).toHaveBeenLastCalledWith('supplier', { limit: 20, offset: 0 }) + wrapper.unmount() + }) + + test('翻到第二页时按页大小换算出 offset', async () => { + partnersApi.list.mockResolvedValue({ data: { items: [], total: 40 } }) + const wrapper = mountPartner() + await flushPromises() + + await wrapper.find('.btn-next').trigger('click') + await flushPromises() + + expect(partnersApi.list).toHaveBeenLastCalledWith('supplier', { limit: 20, offset: 20 }) + wrapper.unmount() + }) + + test('切换角色时页码归零,不会停在上一个角色的第 N 页', async () => { + partnersApi.list.mockResolvedValue({ data: { items: [], total: 40 } }) + const wrapper = mountPartner() + await flushPromises() + + await wrapper.find('.btn-next').trigger('click') + await flushPromises() + expect(partnersApi.list).toHaveBeenLastCalledWith('supplier', { limit: 20, offset: 20 }) + + await wrapper.setProps({ role: 'customer' }) + await flushPromises() + + expect(partnersApi.list).toHaveBeenLastCalledWith('customer', { limit: 20, offset: 0 }) + wrapper.unmount() + }) + + test('总数透传给分页组件,用户能看到一共多少条', async () => { + partnersApi.list.mockResolvedValue({ data: { items: [], total: 42 } }) + const wrapper = mountPartner() + await flushPromises() + + expect(wrapper.text()).toContain('共 42 条') + wrapper.unmount() + }) +}) diff --git a/frontend/src/views/partners/PartnerList.vue b/frontend/src/views/partners/PartnerList.vue index ab5cd4cc..6fef39f0 100644 --- a/frontend/src/views/partners/PartnerList.vue +++ b/frontend/src/views/partners/PartnerList.vue @@ -4,7 +4,15 @@

{{ title }}

业务伙伴独立于本公司的组织数据,可按合作组织配置{{ title }}身份和状态。

新增{{ title }} - + @@ -12,8 +20,7 @@ - - + @@ -39,6 +46,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue' import { ElMessage } from 'element-plus' import { partnersApi } from '../../api/partners' import { replaceAllKeys } from '../../utils/replaceAllKeys' +import BaseTable from '../../components/business/BaseTable.vue' const props = defineProps({ role: { type: String, required: true }, title: { type: String, required: true } }) const rows = ref([]) @@ -46,6 +54,8 @@ const loading = ref(false) const saving = ref(false) const dialogVisible = ref(false) const editingId = ref(null) +// 每页条数跟物料等列表页保持一致,避免同一个系统里两种翻页手感 +const pagination = reactive({ currentPage: 1, pageSize: 20, total: 0 }) const statusField = computed(() => `${props.role}_status`) // 合作状态英文枚举到中文的对照。列表列与编辑弹窗共用这一份,避免两处各写一遍后失步。 const STATUS_LABELS = { active: '启用', suspended: '暂停', eliminated: '淘汰', pending: '待审核', inactive: '停用' } @@ -54,7 +64,20 @@ function statusFormatter(row, column, cellValue) { return STATUS_LABELS[cellValu const form = reactive(emptyForm()) function emptyForm() { return { partner_code: '', legal_name: '', unified_credit_code: '', contact_name: '', contact_phone: '', address: '', status: 'active', is_supplier: false, is_customer: false } } -async function load() { loading.value = true; try { const response = await partnersApi.list(props.role); rows.value = response.data || [] } finally { loading.value = false } } +async function load(currentPage = pagination.currentPage) { + loading.value = true + try { + const offset = (currentPage - 1) * pagination.pageSize + const response = await partnersApi.list(props.role, { limit: pagination.pageSize, offset }) + rows.value = response.data?.items || [] + pagination.total = response.data?.total || 0 + pagination.currentPage = currentPage + } finally { + loading.value = false + } +} +// 换角色等于换了一份数据集,页码必须归零,否则会停在上一个角色的第 N 页上 +watch(() => props.role, () => load(1)) // 用 replaceAllKeys 而不是 Object.assign:后者删不掉上次编辑残留的 id, // 新增时会把别人的 id 一起提交上去。 function openCreate() { editingId.value = null; replaceAllKeys(form, emptyForm()); dialogVisible.value = true } @@ -71,10 +94,10 @@ async function save() { if (!form.partner_code.trim()) { ElMessage.warning('请填写伙伴编码'); return } if (!form.legal_name.trim()) { ElMessage.warning('请填写企业名称'); return } saving.value = true + // 保存后停在第几页由 load 的默认参数决定:留在当前页,不把用户甩回第一页 try { if (editingId.value) await partnersApi.update(editingId.value, payload()); else await partnersApi.create(payload()); ElMessage.success('保存成功'); dialogVisible.value = false; await load() } finally { saving.value = false } } -watch(() => props.role, load) -onMounted(load) +onMounted(() => load(1))