前端问题优化
This commit is contained in:
parent
85d9683754
commit
837cc5787b
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
>
|
||||
<slot />
|
||||
<template #empty>
|
||||
<span>暂无数据</span>
|
||||
<span>{{ emptyText }}</span>
|
||||
</template>
|
||||
</el-table>
|
||||
<BasePagination
|
||||
|
|
@ -37,7 +37,11 @@ defineProps({
|
|||
},
|
||||
currentPage: Number,
|
||||
pageSize: Number,
|
||||
total: Number
|
||||
total: Number,
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: '暂无数据'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:currentPage', 'page-change', 'selection-change'])
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ describe('伙伴表单必填校验', () => {
|
|||
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,15 @@
|
|||
<div><h2>{{ title }}</h2><p>业务伙伴独立于本公司的组织数据,可按合作组织配置{{ title }}身份和状态。</p></div>
|
||||
<el-button type="primary" @click="openCreate">新增{{ title }}</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" stripe>
|
||||
<BaseTable
|
||||
:data="rows"
|
||||
:loading="loading"
|
||||
:empty-text="`暂无${title}`"
|
||||
:current-page="pagination.currentPage"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@page-change="load"
|
||||
>
|
||||
<el-table-column prop="partner_code" label="伙伴编码" width="140" />
|
||||
<el-table-column prop="legal_name" label="企业名称" min-width="180" />
|
||||
<el-table-column prop="unified_credit_code" label="统一社会信用代码" min-width="180" />
|
||||
|
|
@ -12,8 +20,7 @@
|
|||
<el-table-column prop="contact_phone" label="联系电话" width="140" />
|
||||
<el-table-column :prop="statusField" label="合作状态" width="110" :formatter="statusFormatter" />
|
||||
<el-table-column label="操作" width="100"><template #default="{ row }"><el-button link type="primary" @click="openEdit(row)">编辑</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && rows.length === 0" :description="`暂无${title}`" />
|
||||
</BaseTable>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="`${editingId ? '编辑' : '新增'}${title}`" width="560px">
|
||||
<el-form :model="form" label-width="130px">
|
||||
|
|
@ -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))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -283,12 +283,32 @@ func (h *Handler) ListPartners(c *gin.Context) {
|
|||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.partnerSvc.List(orgID, c.Query("role"))
|
||||
// 分页参数的默认值与上限跟物料等列表接口保持一致,前端不必记两套规则
|
||||
limit := 20
|
||||
offset := 0
|
||||
if l := c.Query("limit"); l != "" {
|
||||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||||
limit = parsed
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
}
|
||||
}
|
||||
if o := c.Query("offset"); o != "" {
|
||||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||||
offset = parsed
|
||||
}
|
||||
}
|
||||
|
||||
items, total, err := h.partnerSvc.List(orgID, c.Query("role"), limit, offset)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.Success(c, items)
|
||||
if items == nil {
|
||||
items = []model.BusinessPartner{}
|
||||
}
|
||||
response.PaginatedSuccess(c, items, limit, offset, total)
|
||||
}
|
||||
func (h *Handler) CreatePartner(c *gin.Context) {
|
||||
orgID, ok := organizationID(c)
|
||||
|
|
|
|||
|
|
@ -11,28 +11,39 @@ type PartnerRepository struct{}
|
|||
|
||||
func NewPartnerRepository() *PartnerRepository { return &PartnerRepository{} }
|
||||
|
||||
func (r *PartnerRepository) List(organizationID int64, role string) ([]model.BusinessPartner, error) {
|
||||
func (r *PartnerRepository) List(organizationID int64, role string, limit, offset int) ([]model.BusinessPartner, int64, error) {
|
||||
// 角色过滤写成一份,由计数和取数共用:两处各写一遍,改一处漏一处就会让
|
||||
// 总数和列表对不上(总数算的是全部,列表却只有供应商)。
|
||||
filter := `FROM organization_partners op
|
||||
JOIN business_partners bp ON bp.id = op.business_partner_id
|
||||
WHERE op.organization_id = ?`
|
||||
args := []interface{}{organizationID}
|
||||
if role == "supplier" {
|
||||
filter += ` AND op.is_supplier = TRUE`
|
||||
}
|
||||
if role == "customer" {
|
||||
filter += ` AND op.is_customer = TRUE`
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := database.DB.QueryRow(`SELECT COUNT(*) `+filter, args...).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
query := `SELECT op.id, op.organization_id, bp.id, op.partner_code, bp.legal_name,
|
||||
COALESCE(bp.unified_credit_code, ''), COALESCE(bp.contact_name, ''),
|
||||
COALESCE(bp.contact_phone, ''), COALESCE(bp.address, ''),
|
||||
op.is_supplier, op.is_customer, COALESCE(op.supplier_status, ''),
|
||||
COALESCE(op.customer_status, ''), COALESCE(op.settlement_currency, ''),
|
||||
COALESCE(op.payment_terms, '')
|
||||
FROM organization_partners op
|
||||
JOIN business_partners bp ON bp.id = op.business_partner_id
|
||||
WHERE op.organization_id = ?`
|
||||
args := []interface{}{organizationID}
|
||||
if role == "supplier" {
|
||||
query += ` AND op.is_supplier = TRUE`
|
||||
}
|
||||
if role == "customer" {
|
||||
query += ` AND op.is_customer = TRUE`
|
||||
}
|
||||
query += ` ORDER BY op.partner_code`
|
||||
` + filter + `
|
||||
ORDER BY op.partner_code
|
||||
LIMIT ? OFFSET ?`
|
||||
|
||||
rows, err := database.DB.Query(query, args...)
|
||||
// 计数查询已经用完 args,这里追加分页参数不会再影响到它
|
||||
rows, err := database.DB.Query(query, append(args, limit, offset)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]model.BusinessPartner, 0)
|
||||
|
|
@ -42,11 +53,11 @@ func (r *PartnerRepository) List(organizationID int64, role string) ([]model.Bus
|
|||
&item.LegalName, &item.UnifiedCreditCode, &item.ContactName, &item.ContactPhone,
|
||||
&item.Address, &item.IsSupplier, &item.IsCustomer, &item.SupplierStatus,
|
||||
&item.CustomerStatus, &item.SettlementCurrency, &item.PaymentTerms); err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
return items, total, rows.Err()
|
||||
}
|
||||
|
||||
func (r *PartnerRepository) Create(item *model.BusinessPartner) (err error) {
|
||||
|
|
|
|||
|
|
@ -199,11 +199,11 @@ type PartnerService struct{ repo *repository.PartnerRepository }
|
|||
func NewPartnerService() *PartnerService {
|
||||
return &PartnerService{repo: repository.NewPartnerRepository()}
|
||||
}
|
||||
func (s *PartnerService) List(orgID int64, role string) ([]model.BusinessPartner, error) {
|
||||
func (s *PartnerService) List(orgID int64, role string, limit, offset int) ([]model.BusinessPartner, int64, error) {
|
||||
if role != "" && role != "supplier" && role != "customer" {
|
||||
return nil, errors.New("role must be supplier or customer")
|
||||
return nil, 0, errors.New("role must be supplier or customer")
|
||||
}
|
||||
return s.repo.List(orgID, role)
|
||||
return s.repo.List(orgID, role, limit, offset)
|
||||
}
|
||||
|
||||
// Create 新建伙伴。请求体可能带着前端表单残留的 id,这里不读取 item.ID,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user